Callbacks: o começo de tudo

[99] Callbacks: o começo de tudo

Antes das Promises, esperar era passar uma função para ser chamada depois. O artigo apresenta o callback assíncrono, a convenção do Node em que o erro vem sempre no primeiro argumento, a pirâmide que nasce quando uma operação depende da anterior, e os casos em que o callback continua sendo a escolha certa.
Javascript

20 min de leitura

No artigo O que é Programação Assíncrona? O Event Loop Explicado entendemos o Event Loop — o mecanismo que permite ao JavaScript ser assíncrono mesmo sendo single-threaded. Agora vamos entender o primeiro padrão que surgiu para lidar com essa assincronicidade: os callbacks.

Callbacks não são exclusivos do código assíncrono — você já os usou bastante nos módulos anteriores. Mas é no contexto assíncrono que eles revelam tanto seu poder quanto seus problemas.

O que é um callback?

Um callback é simplesmente uma função passada como argumento para outra função, que será executada em algum momento — imediatamente ou depois.

// Callback síncrono — executado imediatamente
const numeros = [3, 1, 4, 1, 5, 9];

numeros.forEach(function(numero) {
  console.log(numero); // esta função é um callback
});

// Com arrow function — mais comum
numeros.forEach(numero => console.log(numero));

// filter também usa callback
const pares = numeros.filter(n => n % 2 === 0);

Você já usou callbacks o tempo todo — forEach, map, filter, reduce, addEventListener — todos recebem callbacks. A diferença agora é que vamos usar callbacks para operações assíncronas.

Callbacks assíncronos

Um callback assíncrono é executado depois — quando uma operação demorada termina:

console.log("Antes");

// O callback só executa após 2 segundos
setTimeout(function() {
  console.log("Dentro do callback");
}, 2000);

console.log("Depois");

// Saída:
// Antes
// Depois
// (2 segundos depois)
// Dentro do callback

O JavaScript não espera — ele registra o callback e continua. Quando o tempo passa, o Event Loop coloca o callback na fila para executar.

Simulando operações assíncronas

Em exemplos didáticos, usamos setTimeout para simular operações que levam tempo — como buscar dados de um servidor:

function buscarUsuario(id, callback) {
  console.log(`Buscando usuário ${id}...`);

  // Simula 1.5 segundos de espera (como uma requisição real)
  setTimeout(function() {
    const usuario = {
      id,
      nome: "Ana Paula",
      email: "ana@email.com",
      plano: "premium",
    };
    callback(usuario); // chama o callback com o resultado
  }, 1500);
}

// Usando a função — passamos o que fazer com o resultado
buscarUsuario(42, function(usuario) {
  console.log(`Usuário encontrado: ${usuario.nome}`);
  console.log(`Plano: ${usuario.plano}`);
});

console.log("Código continua executando...");

// Saída:
// Buscando usuário 42...
// Código continua executando...
// (1.5 segundos depois)
// Usuário encontrado: Ana Paula
// Plano: premium

O padrão error-first callback

O Node.js popularizou uma convenção importante para callbacks assíncronos: o primeiro parâmetro é sempre o erro, e o segundo é o resultado. Isso garante consistência no tratamento de falhas:

function buscarProduto(id, callback) {
  setTimeout(function() {
    if (id <= 0) {
      // Primeiro argumento: o erro
      callback(new Error("ID inválido — deve ser maior que zero."), null);
      return;
    }

    if (id > 100) {
      callback(new Error(`Produto ${id} não encontrado.`), null);
      return;
    }

    // Segundo argumento: o resultado
    callback(null, {
      id,
      nome: "Notebook Pro",
      preco: 3500,
      estoque: 15,
    });
  }, 1000);
}

// Usando — sempre verifique o erro primeiro
buscarProduto(42, function(erro, produto) {
  if (erro) {
    console.error(`Erro: ${erro.message}`);
    return; // para aqui se houver erro
  }

  console.log(`Produto: ${produto.nome}`);
  console.log(`Preço: R$ ${produto.preco}`);
});

buscarProduto(-5, function(erro, produto) {
  if (erro) {
    console.error(`Erro: ${erro.message}`); // Erro: ID inválido
    return;
  }
  console.log(produto.nome);
});

O padrão é sempre: callback(erro, resultado). Se deu certo, erro é null. Se deu errado, resultado é null.

Callbacks aninhados — operações dependentes

O problema real começa quando uma operação depende do resultado de outra:

function buscarUsuario(id, cb) {
  setTimeout(() => {
    cb(null, { id, nome: "Carlos", enderecoId: 7 });
  }, 800);
}

function buscarEndereco(enderecoId, cb) {
  setTimeout(() => {
    cb(null, { id: enderecoId, rua: "Av. Brasil", cidade: "São Paulo" });
  }, 600);
}

function buscarPedidos(usuarioId, cb) {
  setTimeout(() => {
    cb(null, [
      { id: 101, total: 250 },
      { id: 102, total: 180 },
    ]);
  }, 700);
}

function calcularFrete(cidade, cb) {
  setTimeout(() => {
    cb(null, { cidade, valor: 25.90, prazo: "3 dias úteis" });
  }, 500);
}

// Para fazer tudo isso em sequência — chegamos ao Callback Hell
buscarUsuario(1, function(erro, usuario) {
  if (erro) return console.error(erro);

  buscarEndereco(usuario.enderecoId, function(erro, endereco) {
    if (erro) return console.error(erro);

    buscarPedidos(usuario.id, function(erro, pedidos) {
      if (erro) return console.error(erro);

      calcularFrete(endereco.cidade, function(erro, frete) {
        if (erro) return console.error(erro);

        // Finalmente chegamos ao resultado
        console.log(`Usuário: ${usuario.nome}`);
        console.log(`Cidade: ${endereco.cidade}`);
        console.log(`Pedidos: ${pedidos.length}`);
        console.log(`Frete: R$ ${frete.valor} — ${frete.prazo}`);

        // E se precisarmos de mais um passo? Mais um nível...
      });
    });
  });
});

Isso é o Callback Hell — também chamado de "pyramid of doom" pela forma que o código toma. Os problemas são claros:

  • Difícil de ler — cresce para a direita indefinidamente
  • Difícil de manter — alterar a ordem é trabalhoso
  • Difícil de tratar erros — cada nível precisa verificar o erro
  • Impossível de reusar — lógica toda acoplada

Amenizando o Callback Hell — funções nomeadas

Uma solução parcial é extrair os callbacks em funções nomeadas:

// Em vez de aninhar tudo, quebramos em funções nomeadas

function aoReceberUsuario(erro, usuario) {
  if (erro) return tratarErro(erro);
  buscarEndereco(usuario.enderecoId, aoReceberEndereco.bind(null, usuario));
}

function aoReceberEndereco(usuario, erro, endereco) {
  if (erro) return tratarErro(erro);
  buscarPedidos(usuario.id, aoReceberPedidos.bind(null, usuario, endereco));
}

function aoReceberPedidos(usuario, endereco, erro, pedidos) {
  if (erro) return tratarErro(erro);
  calcularFrete(endereco.cidade, aoReceberFrete.bind(null, usuario, endereco, pedidos));
}

function aoReceberFrete(usuario, endereco, pedidos, erro, frete) {
  if (erro) return tratarErro(erro);
  console.log(`Usuário: ${usuario.nome}`);
  console.log(`Cidade: ${endereco.cidade}`);
  console.log(`Pedidos: ${pedidos.length}`);
  console.log(`Frete: R$ ${frete.valor}`);
}

function tratarErro(erro) {
  console.error(`Erro: ${erro.message}`);
}

// Inicia a cadeia
buscarUsuario(1, aoReceberUsuario);

Melhor — pelo menos o código não cresce para a direita. Mas ainda é complicado gerenciar o estado entre os níveis e o fluxo não é linear e legível.

Callbacks em paralelo

Às vezes as operações não dependem umas das outras — podem executar simultaneamente. O desafio é saber quando todas terminaram:

function buscarDadosParalelo(ids, callback) {
  const resultados = [];
  let concluidos = 0;
  let houveErro = false;

  ids.forEach((id, index) => {
    buscarProduto(id, function(erro, produto) {
      if (houveErro) return; // já deu errado em outro

      if (erro) {
        houveErro = true;
        return callback(erro, null);
      }

      resultados[index] = produto; // mantém a ordem
      concluidos++;

      if (concluidos === ids.length) {
        callback(null, resultados); // todos concluíram!
      }
    });
  });
}

buscarDadosParalelo([1, 5, 12, 30], function(erro, produtos) {
  if (erro) return console.error(erro);
  console.log(`${produtos.length} produtos carregados.`);
  produtos.forEach(p => console.log(`- ${p.nome}: R$ ${p.preco}`));
});

Isso funciona, mas é verboso e propenso a bugs. As Promises resolvem isso com muito mais elegância — como veremos no próximo artigo.

Casos onde callbacks ainda são a escolha certa

Apesar dos problemas, callbacks são a solução ideal em vários cenários:

// 1. Eventos do DOM — chamados várias vezes
botao.addEventListener("click", (e) => {
  console.log("Clicado!");
});

// 2. Métodos de array — síncronos e funcionais
const dobrados = [1, 2, 3].map(n => n * 2);
const pares = [1, 2, 3, 4].filter(n => n % 2 === 0);

// 3. Operações simples com setTimeout
setTimeout(() => limparMensagem(), 3000);

// 4. APIs que retornam múltiplos eventos ao longo do tempo
// (Streams, WebSockets — Promises só resolvem uma vez)
stream.on("data", (chunk) => processar(chunk));
stream.on("end", () => finalizar());
stream.on("error", (err) => tratarErro(err));

Para operações únicas que levam tempo e podem falhar, as Promises são superiores. Para eventos recorrentes, callbacks são a solução natural.

Exemplo completo — sistema de notificações com callbacks

// Sistema que demonstra callbacks de forma organizada

const sistemaBD = {
  usuarios: [
    { id: 1, nome: "Ana", email: "ana@email.com", notificacoesAtivas: true },
    { id: 2, nome: "Bruno", email: "bruno@email.com", notificacoesAtivas: false },
    { id: 3, nome: "Clara", email: "clara@email.com", notificacoesAtivas: true },
  ],

  buscarUsuario(id, cb) {
    setTimeout(() => {
      const usuario = this.usuarios.find(u => u.id === id);
      if (!usuario) return cb(new Error(`Usuário ${id} não encontrado.`));
      cb(null, usuario);
    }, 500);
  },

  buscarTodos(cb) {
    setTimeout(() => {
      cb(null, [...this.usuarios]);
    }, 600);
  },
};

const emailService = {
  enviar(destinatario, mensagem, cb) {
    setTimeout(() => {
      if (!destinatario.includes("@")) {
        return cb(new Error(`E-mail inválido: ${destinatario}`));
      }
      console.log(`📧 E-mail enviado para ${destinatario}: "${mensagem}"`);
      cb(null, { enviado: true, destinatario });
    }, 300);
  },
};

// Notificar um único usuário
function notificarUsuario(id, mensagem, cb) {
  sistemaBD.buscarUsuario(id, function(erro, usuario) {
    if (erro) return cb(erro);

    if (!usuario.notificacoesAtivas) {
      return cb(null, { enviado: false, motivo: "Notificações desativadas." });
    }

    emailService.enviar(usuario.email, mensagem, function(erro, resultado) {
      if (erro) return cb(erro);
      cb(null, { ...resultado, usuario: usuario.nome });
    });
  });
}

// Notificar todos os usuários com notificações ativas
function notificarTodos(mensagem, cb) {
  sistemaBD.buscarTodos(function(erro, usuarios) {
    if (erro) return cb(erro);

    const ativos = usuarios.filter(u => u.notificacoesAtivas);
    const resultados = [];
    let concluidos = 0;

    if (ativos.length === 0) return cb(null, []);

    ativos.forEach(usuario => {
      emailService.enviar(usuario.email, mensagem, function(erro, resultado) {
        concluidos++;

        if (erro) {
          resultados.push({ usuario: usuario.nome, erro: erro.message });
        } else {
          resultados.push({ usuario: usuario.nome, ...resultado });
        }

        if (concluidos === ativos.length) {
          cb(null, resultados);
        }
      });
    });
  });
}

// Testando
console.log("Iniciando sistema de notificações...\n");

notificarUsuario(1, "Seu pedido foi aprovado!", function(erro, resultado) {
  if (erro) return console.error(`Erro: ${erro.message}`);
  console.log(`Resultado para usuário 1:`, resultado);
});

notificarUsuario(2, "Promoção especial para você!", function(erro, resultado) {
  if (erro) return console.error(`Erro: ${erro.message}`);
  console.log(`Resultado para usuário 2:`, resultado);
});

notificarTodos("Manutenção programada para domingo.", function(erro, resultados) {
  if (erro) return console.error(`Erro geral: ${erro.message}`);
  console.log("\nResultados do envio em massa:");
  resultados.forEach(r => console.log(` - ${r.usuario}: ${r.enviado ? "✓" : "✗"}`));
});

Boas práticas com callbacks

// ✅ 1. Sempre siga o padrão error-first
function operacao(params, callback) {
  // callback(erro, resultado)
  callback(null, resultado); // sucesso
  callback(new Error("msg"), null); // falha
}

// ✅ 2. Sempre verifique o erro primeiro
operacao(params, function(erro, resultado) {
  if (erro) {
    console.error(erro);
    return; // pare aqui
  }
  // use resultado
});

// ✅ 3. Nunca chame o callback duas vezes
//    A guarda só faz sentido quando há mais de um caminho capaz de
//    responder — aqui, o dado e o limite de tempo disputam.
function operacaoSegura(cb) {
  let respondeu = false;

  const responder = (erro, dado) => {
    if (respondeu) return; // a segunda resposta é descartada
    respondeu = true;
    cb(erro, dado);
  };

  buscarDados(responder);
  setTimeout(() => responder(new Error("tempo esgotado")), 5000);

  // Atenção: isto para de OUVIR a operação, não a cancela —
  // ela continua correndo. Cancelar de verdade exige AbortController.
}

// ✅ 4. Extraia callbacks em funções nomeadas
//    para evitar o Callback Hell
function aoReceberDados(erro, dados) { /* ... */ }
operacao(params, aoReceberDados);

// ✅ 5. Para múltiplas operações assíncronas,
//    prefira Promises ou async/await (próximos artigos)

Tarefa para você

Implemente um sistema de carrinho de compras assíncrono usando apenas callbacks:

// Funções disponíveis (implemente com setTimeout):
// buscarProduto(id, callback)  → retorna produto ou erro
// verificarEstoque(id, quantidade, callback) → retorna true/false ou erro
// aplicarCupom(codigo, subtotal, callback) → retorna valor com desconto ou erro
// finalizarPedido(itens, total, callback) → retorna pedido ou erro

// O fluxo deve ser:
// 1. Buscar 2 produtos (em paralelo)
// 2. Verificar estoque de ambos (em paralelo)
// 3. Calcular subtotal
// 4. Aplicar cupom (se fornecido)
// 5. Finalizar pedido
// 6. Exibir resumo ou erro em cada etapa

// Dica: use o padrão de paralelo que vimos para os passos 1 e 2
Ver solução — o carrinho inteiro só com callbacks — inclusive os dois passos em paralelo
// Regra do exercício: nada de Promise, nada de async/await. Só o padrão
// callback(erro, dado) — o mesmo que o Node usou por anos.

const CATALOGO = {
  1: { id: 1, nome: "Caneca", preco: 39.9 },
  2: { id: 2, nome: "Camiseta", preco: 79.9 },
  3: { id: 3, nome: "Adesivo", preco: 5.0 },
};

const ESTOQUE = { 1: 10, 2: 3, 3: 0 };
const CUPONS = { PRIMEIRA10: 0.1, BLACK30: 0.3 };

const latencia = () => 100 + Math.random() * 200;

// ---------------------------------------------------------------
// As quatro funções — sempre (erro, dado), sempre nessa ordem
// ---------------------------------------------------------------
function buscarProduto(id, callback) {
  setTimeout(() => {
    const produto = CATALOGO[id];

    if (!produto) {
      // O erro vai no PRIMEIRO argumento. É convenção, não capricho:
      // é o que permite `if (erro)` como primeira linha de todo
      // callback, sem precisar saber de que função ele veio.
      callback(new Error(`Produto ${id} não existe`));
      return;
    }

    callback(null, produto);
  }, latencia());
}

function verificarEstoque(id, quantidade, callback) {
  setTimeout(() => {
    const disponivel = ESTOQUE[id] ?? 0;

    if (disponivel < quantidade) {
      callback(new Error(
        `Estoque insuficiente do produto ${id}: pedidas ${quantidade}, há ${disponivel}`
      ));
      return;
    }

    callback(null, true);
  }, latencia());
}

function aplicarCupom(codigo, subtotal, callback) {
  setTimeout(() => {
    if (!codigo) {
      callback(null, { total: subtotal, desconto: 0 }); // sem cupom não é erro
      return;
    }

    const percentual = CUPONS[codigo];

    if (percentual === undefined) {
      callback(new Error(`Cupom inválido: ${codigo}`));
      return;
    }

    const desconto = subtotal * percentual;
    callback(null, { total: subtotal - desconto, desconto });
  }, latencia());
}

function finalizarPedido(itens, total, callback) {
  setTimeout(() => {
    if (total <= 0) {
      callback(new Error("Total inválido para finalizar"));
      return;
    }

    callback(null, {
      numero: `PED-${Date.now().toString().slice(-6)}`,
      itens,
      total,
      criadoEm: new Date().toLocaleString("pt-BR"),
    });
  }, latencia());
}

// ---------------------------------------------------------------
// O padrão paralelo, escrito uma vez e reaproveitado
// ---------------------------------------------------------------
// Sem Promise.all, o paralelismo é feito à mão: dispara tudo de uma
// vez, conta quantos voltaram e só chama o callback final no último.
function emParalelo(tarefas, callback) {
  const resultados = new Array(tarefas.length);
  let pendentes = tarefas.length;
  let jaFalhou = false;

  if (pendentes === 0) {
    callback(null, []);
    return;
  }

  tarefas.forEach((tarefa, indice) => {
    tarefa((erro, valor) => {
      // A guarda existe porque as tarefas continuam correndo depois da
      // primeira falha. Sem ela, o callback final seria chamado duas
      // vezes — e chamar callback duas vezes é o bug mais difícil de
      // achar neste estilo de código.
      if (jaFalhou) return;

      if (erro) {
        jaFalhou = true;
        callback(erro);
        return;
      }

      // A posição preserva a ordem original: quem termina primeiro não
      // "passa na frente" no array de resultados.
      resultados[indice] = valor;
      pendentes--;

      if (pendentes === 0) callback(null, resultados);
    });
  });
}

// ---------------------------------------------------------------
// O fluxo dos seis passos
// ---------------------------------------------------------------
function comprar(pedido, callback) {
  const { itens, cupom } = pedido;

  // 1. os dois produtos, em paralelo
  emParalelo(
    itens.map((item) => (cb) => buscarProduto(item.id, cb)),
    (erro, produtos) => {
      if (erro) return callback(erro);

      // 2. estoque dos dois, também em paralelo
      emParalelo(
        itens.map((item) => (cb) => verificarEstoque(item.id, item.quantidade, cb)),
        (erro) => {
          if (erro) return callback(erro);

          // 3. subtotal
          const subtotal = produtos.reduce(
            (soma, produto, i) => soma + produto.preco * itens[i].quantidade,
            0
          );

          // 4. cupom
          aplicarCupom(cupom, subtotal, (erro, { total, desconto }) => {
            if (erro) return callback(erro);

            // 5. fecha
            const linhas = produtos.map((produto, i) => ({
              ...produto,
              quantidade: itens[i].quantidade,
            }));

            finalizarPedido(linhas, total, (erro, pedidoCriado) => {
              if (erro) return callback(erro);

              callback(null, { ...pedidoCriado, subtotal, desconto });
            });
          });
        }
      );
    }
  );
}

// ---------------------------------------------------------------
// 6 — exibindo
// ---------------------------------------------------------------
const reais = (n) => n.toLocaleString("pt-BR", { style: "currency", currency: "BRL" });

comprar(
  { itens: [{ id: 1, quantidade: 2 }, { id: 2, quantidade: 1 }], cupom: "PRIMEIRA10" },
  (erro, pedido) => {
    if (erro) {
      console.error(`❌ ${erro.message}`);
      return;
    }

    console.log(`✅ Pedido ${pedido.numero} — ${pedido.criadoEm}`);
    pedido.itens.forEach((i) =>
      console.log(`   ${i.quantidade}x ${i.nome} — ${reais(i.preco * i.quantidade)}`)
    );
    console.log(`   Subtotal: ${reais(pedido.subtotal)}`);
    console.log(`   Desconto: -${reais(pedido.desconto)}`);
    console.log(`   Total:    ${reais(pedido.total)}`);
  }
);

// Um caminho de erro, para ver a mensagem certa aparecer:
comprar({ itens: [{ id: 3, quantidade: 1 }], cupom: null }, (erro) => {
  if (erro) console.error(`❌ ${erro.message}`); // Estoque insuficiente do produto 3
});

// ---------------------------------------------------------------
// O detalhe que morde: `return callback(erro)`
// ---------------------------------------------------------------
// O `return` na frente de cada chamada de erro não devolve valor a
// ninguém — serve só para PARAR a função ali. Sem ele, o código segue
// e chama o callback de novo mais adiante, com sucesso, depois de já
// ter reportado falha. O consumidor recebe as duas coisas e acredita
// na última.
//
// É a diferença entre isto:
//
//   if (erro) callback(erro);        // ❌ segue executando
//   const total = calcular(dados);   // dados é undefined aqui
//
// e isto:
//
//   if (erro) return callback(erro); // ✅ para de verdade

Duas regras salvam o estilo callback: o erro é sempre o primeiro argumento, e toda chamada de erro vem com return na frente. Sem a segunda, a função continua e chama o callback duas vezes — falha e sucesso para a mesma operação, com o consumidor acreditando no que chegou por último.

Callback não é conceito novo deste módulo: toda vez que você passou uma função para map, filter ou addEventListener, era disso que se tratava. O que a assincronia muda é o momento da chamada — e é essa mudança que torna o return inútil, que obriga ao padrão erro-primeiro do Node e que faz o código crescer para a direita quando uma operação depende do resultado da anterior.

Fontes e Referências

Exercícios

Exercício 1

O consumidor está escrito corretamente, com return depois de tratar o erro. Mesmo assim, o que ele imprime?

function buscar(id, cb) {
  setTimeout(() => {
    if (id <= 0) cb(new Error("id inválido"), null);
    cb(null, { id, nome: "Produto" });
  }, 100);
}

buscar(-1, (erro, produto) => {
  if (erro) {
    console.error("erro:", erro.message);
    return;
  }
  console.log("ok:", produto.nome);
});
Ver resposta

✓ Resposta: Imprime erro: id inválido e, logo em seguida, ok: Produto. O consumidor recebe as duas coisas para a mesma operação: falhou e deu certo. O return dele funciona, mas encerra apenas aquela invocação do callback — não impede que o produtor chame o callback de novo, porque o if lá dentro não tem return e a execução segue para a linha de sucesso. É por isso que a convenção do estilo callback é escrever sempre return cb(erro), ou pôr o return na linha seguinte: o return não devolve valor a ninguém, ele existe só para parar a função ali. O sintoma na aplicação é cruel porque o segundo resultado sobrescreve o primeiro na tela: o usuário vê a mensagem de erro piscar e sumir, e o log fica com as duas linhas, sem que nenhum dos dois estados esteja errado isoladamente.

Exercício 2

Alguém trocou resultados[index] = produto por resultados.push(produto). Cada requisição leva um tempo diferente. O que quebra?

function buscarTudo(ids, callback) {
  const resultados = [];
  let concluidos = 0;

  ids.forEach((id, index) => {
    buscarProduto(id, (erro, produto) => {
      if (erro) return callback(erro);

      resultados.push(produto);
      concluidos++;

      if (concluidos === ids.length) callback(null, resultados);
    });
  });
}
Ver resposta

✓ Resposta: A correspondência entre ids e resultados. Com push, os produtos entram na ordem em que as respostas chegam, que é a ordem das latências, não a ordem em que foram pedidos — resultados[0] deixa de ser o produto de ids[0]. Qualquer código que depois cruze as duas listas por posição, como calcular o subtotal multiplicando pelo quantidade do item correspondente, passa a somar preço de um produto com quantidade de outro. E o defeito é do pior tipo: em desenvolvimento, com servidor local e latência quase igual para todos, as respostas voltam quase sempre na ordem certa e tudo parece funcionar; em produção ele aparece de forma intermitente. A atribuição por índice resolve porque cada resposta escreve na sua própria vaga, reservada desde o começo. Vale reparar de passagem numa fragilidade que continua nas duas versões: quem libera o callback final é o contador, então basta um caminho que esqueça de incrementar concluidos para a operação inteira ficar pendurada para sempre — sem erro, sem retorno e sem tempo limite.

Exercício 3

A busca demora 6 segundos e o limite é de 5. Com a guarda respondeu, o que o consumidor recebe? E sem ela?

function operacaoSegura(cb) {
  let respondeu = false;

  const responder = (erro, dado) => {
    if (respondeu) return;
    respondeu = true;
    cb(erro, dado);
  };

  buscarDados(responder);                                        // volta aos 6s
  setTimeout(() => responder(new Error("tempo esgotado")), 5000); // dispara aos 5s
}
Ver resposta

✓ Resposta: Com a guarda, o consumidor recebe o erro de tempo esgotado, aos 5 segundos; o dado que chega aos 6 é silenciosamente descartado. Sem a guarda, ele recebe o erro aos 5 segundos e o sucesso aos 6 — ou seja, a interface já mostrou a tela de falha e um segundo depois começa a desenhar dados por cima dela, um comportamento que costuma ser reportado como "fantasma" porque não se reproduz quando a rede está boa. Há um ponto que a guarda não resolve e que vale entender: o limite de tempo interrompe a escuta, não a operação. A requisição continua correndo até o fim, ocupando conexão, e se ela tiver efeito colateral no servidor — criar um pedido, cobrar um cartão — esse efeito acontece de qualquer maneira, mesmo que o cliente já tenha desistido. Cancelar de verdade é outro problema, e é exatamente o que o AbortController existe para resolver.

Exercício 4

A função tem cache. O que ela imprime na primeira chamada e o que imprime na segunda?

const cache = {};

function buscar(id, cb) {
  if (cache[id]) return cb(null, cache[id]);

  setTimeout(() => {
    cache[id] = { id, nome: "Produto" };
    cb(null, cache[id]);
  }, 100);
}

let pronto = false;
buscar(1, () => console.log("callback — pronto =", pronto));
pronto = true;
Ver resposta

✓ Resposta: Na primeira vez imprime pronto = true; na segunda, com o cache quente, imprime pronto = false. A mesma função chama o callback de forma assíncrona quando precisa buscar e de forma síncrona quando tem a resposta em mãos — e no caminho síncrono o callback roda antes da linha pronto = true, que ainda nem foi alcançada. O resultado é que qualquer estado preparado logo depois da chamada existe às vezes e às vezes não, e o bug aparece na segunda execução, quando o desenvolvedor já considerou o código pronto. Essa inconsistência é conhecida o bastante para ter apelido — "liberar Zalgo" — e a regra é categórica: um callback deve ser sempre assíncrono ou sempre síncrono, nunca depender do caminho tomado. Para forçar o caminho rápido a esperar, basta adiá-lo uma microtask: queueMicrotask(() => cb(null, cache[id])). As Promises eliminam a categoria inteira por construção, já que o .then nunca executa de forma síncrona, mesmo sobre uma promise já resolvida.

Exercício 5

Quais destes quatro poderiam ser trocados por uma Promise sem perder nada?

botao.addEventListener("click", cb); // A
setTimeout(cb, 1000);                // B
stream.on("data", cb);               // C
buscarUsuario(1, cb);                // D
Ver resposta

✓ Resposta: Só o B e o D. A diferença é quantas vezes o callback é chamado. Uma promise é um recipiente que se resolve exatamente uma vez e guarda esse valor para sempre: pedir de novo devolve o mesmo resultado, e não há como "resolvê-la de novo" com um valor novo. Isso serve perfeitamente para uma espera de tempo e para uma busca — operações únicas, que terminam em sucesso ou falha e acabam ali. Já um botão pode ser clicado cinquenta vezes e um stream entrega muitos pedaços: envolvê-los numa promise capturaria só o primeiro clique e o primeiro pedaço, e todo o resto se perderia sem aviso. Para eventos que se repetem, as ferramentas certas são o próprio callback, o iterador assíncrono com for await...of ou um observable. Vale ainda uma distinção que costuma se embaralhar aqui: a função passada para map ou filter também é um callback, e não tem nada de assíncrona — "callback" descreve como a função é entregue, não quando ela roda.

Comentários

Mais em Javascript

O que é JavaScript e por que aprender?
O que é JavaScript e por que aprender?

Criada em dez dias, em 1995, e ainda assim a única linguagem que todo…

Express.js: o framework web do Node
Express.js: o framework web do Node

O mesmo endpoint que ocupava trinta linhas de if e regex cabe em quatro com…

Trabalhando com datas e horas em JavaScript
Trabalhando com datas e horas em JavaScript

Data parece o tipo mais simples que existe e é o que mais engana: o mês começa…