No artigo anterior 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
function operacaoSegura(cb) {
let chamou = false;
setTimeout(() => {
if (chamou) return;
chamou = true;
cb(null, "resultado");
}, 1000);
}
// ✅ 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
Conclusão
Neste artigo você aprendeu:
- O que é um callback e como já os usávamos nos módulos anteriores
- A diferença entre callbacks síncronos e assíncronos
- O padrão error-first callback do Node.js
- Como callbacks se aninham em operações dependentes
- O Callback Hell e seus problemas reais
- Como amenizar com funções nomeadas
- Como executar callbacks em paralelo
- Quando callbacks ainda são a escolha certa
- Boas práticas para código com callbacks
📚 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/en/docs/guides/error-first-callbacks
- You Don't Know JS: Async & Performance, Cap. 2 — Kyle Simpson: https://github.com/getify/You-Dont-Know-JS
- Eloquent JavaScript, Cap. 11 — Asynchronous Programming: https://eloquentjavascript.net/11_async.html
- Node.js Design Patterns — Mario Casciaro (Packt Publishing)