Callbacks e o callback hell
Nesta aula, exploramos o uso de callbacks para lidar com operações assíncronas em JavaScript, analisamos os problemas que surgem com aninhamento excessivo (callback hell) e introduzimos as Promises como uma solução mais limpa e estruturada.
Callbacks são funções passadas como argumento para outras funções, executadas após a conclusão de uma operação. Em JavaScript, eles são fundamentais para lidar com assincronismo, como requisições de rede, leitura de arquivos ou timers. No entanto, o uso excessivo de callbacks pode levar a um problema conhecido como "callback hell" ou "pyramid of doom", que torna o código difícil de ler e manter. Nesta aula, vamos entender como os callbacks funcionam, identificar os problemas associados e ver como as Promises surgem como uma alternativa mais elegante.
Assincronismo com callbacks
Em JavaScript, operações assíncronas não bloqueiam a execução do código. Em vez disso, elas registram uma função de callback que será chamada quando a operação for concluída. Por exemplo, setTimeout executa um callback após um determinado tempo. Da mesma forma, funções como fs.readFile (no Node.js) usam callbacks para lidar com arquivos.
Exemplo básico de assincronismo com callback:
console.log('Início');
setTimeout(() => {
console.log('Dentro do callback');
}, 2000);
console.log('Fim');
A saída será: "Início", "Fim" e, após 2 segundos, "Dentro do callback". Isso mostra que o código não espera o timer terminar para continuar. Callbacks são a base do padrão de programação assíncrona em JavaScript.
Problemas
O principal problema com callbacks é o aninhamento excessivo. Quando precisamos executar múltiplas operações assíncronas em sequência, cada uma dependendo do resultado da anterior, o código tende a se tornar profundamente aninhado, criando a temida "pirâmide da desgraça" (callback hell).
Exemplo de callback hell:
getUser(id, (err, user) => {
if (err) {
console.error(err);
} else {
getPosts(user.id, (err, posts) => {
if (err) {
console.error(err);
} else {
getComments(posts[0].id, (err, comments) => {
if (err) {
console.error(err);
} else {
console.log(comments);
}
});
}
});
}
});
Esse código é difícil de ler, depurar e manter. Além disso, o tratamento de erros é repetitivo e propenso a falhas. Outros problemas incluem:
- Inversão de controle: a lógica do programa fica nas mãos das funções de callback, dificultando o fluxo.
- Dificuldade de paralelismo: executar callbacks em paralelo requer lógica adicional.
- Reuso limitado: callbacks aninhados não são facilmente reutilizáveis.
Caminho para Promises
As Promises foram introduzidas no ES6 para resolver os problemas dos callbacks. Uma Promise representa um valor futuro, que pode ser resolvido (sucesso) ou rejeitado (erro). Elas permitem encadeamento de operações assíncronas de forma linear, com tratamento de erros centralizado.
Exemplo com Promise (substituindo o callback hell):
getUser(id)
.then(user => getPosts(user.id))
.then(posts => getComments(posts[0].id))
.then(comments => console.log(comments))
.catch(err => console.error(err));
Muito mais legível! As Promises também suportam execução paralela com Promise.all e Promise.race, e são a base para async/await (ES2017), que torna o código assíncrono ainda mais síncrono na aparência.
Embora os callbacks ainda sejam usados em APIs legadas ou em situações específicas (como eventos), Promises e async/await são o padrão moderno para lidar com assincronismo em JavaScript.
Boas práticas
- Evite aninhar callbacks profundamente; sempre que possível, use Promises ou async/await.
- Nomeie funções callback para melhorar a legibilidade e facilitar a depuração.
- Trate erros em cada nível do callback, ou use um único catch com Promises.
- Considere bibliotecas como
async.jspara gerenciar fluxos complexos de callbacks em projetos legados.
Referências
- MDN: Função de callback
- MDN: Usando Promises
- MDN: Promise
- Node.js: What are callbacks?
- FreeCodeCamp: How to escape callback hell
Exercícios
- Escreva uma função
lerArquivoque simula a leitura de um arquivo usandosetTimeoute um callback. O callback deve receber (erro, dados). Teste com sucesso e erro. - Refatore o código abaixo, que está em callback hell, para usar Promises. Assuma que
getUser,getPostsegetCommentsretornam Promises. - Crie uma função
simularRequisicaoque retorna uma Promise. A Promise deve resolver após 2 segundos com um objeto { dados: 'exemplo' }. UsesetTimeoutpara simular o atraso. - Explique por que o código abaixo é um exemplo de callback hell e como você poderia melhorá-lo.
- Usando
Promise.all, execute duas Promises em paralelo: uma que resolve após 1 segundo com o valor 'A' e outra que resolve após 2 segundos com o valor 'B'. Imprima o array resultante.
function lerArquivo(nome, callback) {
setTimeout(() => {
if (nome === 'erro.txt') {
callback(new Error('Arquivo não encontrado'), null);
} else {
callback(null, 'Conteúdo do arquivo ' + nome);
}
}, 1000);
}
// Teste sucesso
lerArquivo('dados.txt', (err, dados) => {
if (err) console.error(err);
else console.log(dados);
});
// Teste erro
lerArquivo('erro.txt', (err, dados) => {
if (err) console.error(err.message);
else console.log(dados);
});
getUser(id, (err, user) => {
if (err) console.error(err);
else {
getPosts(user.id, (err, posts) => {
if (err) console.error(err);
else {
getComments(posts[0].id, (err, comments) => {
if (err) console.error(err);
else console.log(comments);
});
}
});
}
});
getUser(id)
.then(user => getPosts(user.id))
.then(posts => getComments(posts[0].id))
.then(comments => console.log(comments))
.catch(err => console.error(err));
function simularRequisicao() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ dados: 'exemplo' });
}, 2000);
});
}
simularRequisicao().then(resultado => console.log(resultado));
step1(function(err, result1) {
step2(function(err, result2) {
step3(function(err, result3) {
console.log(result3);
});
});
});
O código é um callback hell porque os callbacks estão aninhados em três níveis, dificultando a leitura e manutenção. Para melhorar, poderíamos usar Promises: se cada step retornar uma Promise, podemos encadear com .then(). Alternativamente, poderíamos nomear as funções callback para evitar aninhamento ou usar async/await.
const promise1 = new Promise(resolve => setTimeout(() => resolve('A'), 1000));
const promise2 = new Promise(resolve => setTimeout(() => resolve('B'), 2000));
Promise.all([promise1, promise2]).then(resultados => console.log(resultados));
// Após 2 segundos, imprime ['A', 'B']