Callbacks em JavaScript: O Padrão Original de Assincronismo
O que é um Callback?
Um callback é simplesmente uma função passada como argumento para outra função, que será executada posteriormente — geralmente após a conclusão de uma operação assíncrona. Em JavaScript, isso é fundamental porque a linguagem é single-threaded: operações como requisições HTTP, leitura de arquivos ou timers não podem bloquear a execução do código. O callback permite que você diga ao JavaScript: "faça isto, e quando terminar, execute essa função".
Quando você compreende callbacks, você entende o núcleo da programação assíncrona em JavaScript. Mesmo com a chegada de Promises e async/await, callbacks continuam sendo o mecanismo subjacente. Se você vai dominar JavaScript profissional, precisa dessa base sólida.
Callbacks Básicos e Seu Funcionamento
O exemplo mais comum é o setTimeout, que executa uma função após um tempo determinado:
function saudar(nome) {
console.log(`Olá, ${nome}!`);
}
setTimeout(function() {
saudar("Maria");
}, 2000); // Executa após 2 segundos
Aqui, a função anônima é o callback. Você passa ela para setTimeout, que a armazena e a executa depois. Outro exemplo prático é com Array:
const numeros = [1, 2, 3, 4, 5];
const dobrados = numeros.map(function(num) {
return num * 2;
});
console.log(dobrados); // [2, 4, 6, 8, 10]
O callback aqui é a função passada ao map(). Esse padrão é tão comum que você provavelmente já usou sem perceber. A função callback recebe parâmetros (no caso, num) que o map() fornece automaticamente. Isso demonstra como callbacks são flexíveis: a função que recebe o callback controla quando executá-lo e com quais argumentos.
Callbacks com Operações Assíncronas Reais
O verdadeiro poder dos callbacks aparece quando você trabalha com operações que levam tempo. Vamos simular uma requisição a um servidor:
function buscarUsuario(id, callback) {
setTimeout(function() {
const usuario = { id: id, nome: "João" };
callback(usuario);
}, 1000);
}
buscarUsuario(1, function(usuario) {
console.log("Usuário encontrado:", usuario);
});
Aqui, buscarUsuario() simula uma requisição que leva 1 segundo. O callback é executado com os dados quando prontos. Em cenários reais com Node.js, você verá este padrão frequentemente:
const fs = require('fs');
fs.readFile('dados.txt', 'utf8', function(erro, dados) {
if (erro) {
console.log("Erro ao ler arquivo:", erro);
return;
}
console.log("Conteúdo:", dados);
});
Aqui temos o padrão error-first callback: o primeiro parâmetro é sempre o erro (ou null se não houver erro), e os dados vêm depois. Esse é o padrão de facto em Node.js e você o verá em muitas bibliotecas legadas.
O Problema: Callback Hell
Aqui está onde callbacks mostram sua limitação. Quando você precisa fazer várias operações sequenciais, o código fica aninhado em níveis cada vez mais profundos:
fs.readFile('usuarios.json', 'utf8', function(erro, dados) {
if (erro) throw erro;
const usuarios = JSON.parse(dados);
fs.readFile('permissoes.json', 'utf8', function(erro2, dados2) {
if (erro2) throw erro2;
const permissoes = JSON.parse(dados2);
fs.writeFile('resultado.json', JSON.stringify({usuarios, permissoes}), function(erro3) {
if (erro3) throw erro3;
console.log("Arquivo salvo!");
});
});
});
Esse "callback hell" ou "pyramid of doom" é difícil de ler, manter e debugar. O tratamento de erros fica espalhado, e rastrear o fluxo se torna complicado. É exatamente por isso que JavaScript evoluiu: Promises surgiram para resolver esse problema, seguidas por async/await.
Nota importante: Você deve entender callbacks profundamente não para usá-los em novo código — embora ainda apareçam em algumas APIs — mas porque muitas bibliotecas antigas os usam, e você provavelmente trabalhar com código legado.
Boas Práticas com Callbacks
Se você precisa usar callbacks (especialmente em código existente), siga estas práticas:
Use nomes descritivos para deixar claro que é um callback:
function processarDados(dados, quandoTerminar) {
setTimeout(function() {
const resultado = dados.map(x => x * 2);
quandoTerminar(resultado);
}, 500);
}
processarDados([1, 2, 3], function(resultado) {
console.log(resultado);
});
Sempre trate erros — nunca suponha que tudo correrá bem:
function operacaoRisca(callback) {
setTimeout(function() {
const sucesso = Math.random() > 0.5;
if (sucesso) {
callback(null, "Operação bem-sucedida");
} else {
callback(new Error("Falha na operação"), null);
}
}, 1000);
}
operacaoRisca(function(erro, resultado) {
if (erro) {
console.error("Erro:", erro.message);
} else {
console.log(resultado);
}
});
Evite callbacks aninhados — separe em funções nomeadas:
function etapa1(callback) {
setTimeout(() => callback(null, "Dados 1"), 300);
}
function etapa2(dados1, callback) {
setTimeout(() => callback(null, dados1 + " + Dados 2"), 300);
}
function etapa3(dados2, callback) {
setTimeout(() => callback(null, dados2 + " = Resultado Final"), 300);
}
etapa1(function(erro, resultado1) {
if (erro) return console.error(erro);
etapa2(resultado1, function(erro, resultado2) {
if (erro) return console.error(erro);
etapa3(resultado2, function(erro, resultado3) {
if (erro) return console.error(erro);
console.log(resultado3);
});
});
});
Conclusão
Callbacks são a base do assincronismo em JavaScript e ainda aparecem em muitas APIs nativas e bibliotecas. O padrão error-first callback é especialmente importante em Node.js. Embora Promises e async/await sejam superiores para novo código, dominar callbacks é essencial para compreender JavaScript profundamente e trabalhar com código legado com confiança. O principal aprendizado é que callbacks permitem executar código depois que uma operação termina, mas em cascatas complexas eles se tornam difíceis de manter — justamente por isso a linguagem evoluiu.