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
- MDN Web Docs — Callback function: https://developer.mozilla.org/pt-BR/docs/Glossary/Callback_function
- MDN Web Docs — Introduction to asynchronous JavaScript: https://developer.mozilla.org/pt-BR/docs/Learn/JavaScript/Asynchronous/Introducing
- JavaScript.info — Introduction: callbacks: https://javascript.info/callbacks
- Node.js Docs — Error-first callbacks: https://nodejs.org/learn/asynchronous-work/javascript-asynchronous-programming-and-callbacks
- Node.js Design Patterns — Mario Casciaro (Packt Publishing)
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 só 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.