Callbacks e o callback hell
Nesta aula, exploramos o conceito de callbacks em JavaScript, como eles permitem programação assíncrona, os problemas do callback hell e a transição para Promises como solução.
Callbacks são funções passadas como argumentos para outras funções, executadas após a conclusão de uma operação. Em JavaScript, eles são fundamentais para lidar com tarefas assíncronas, como leitura de arquivos, requisições de rede ou timers. No entanto, o uso excessivo de callbacks aninhados leva a um problema conhecido como "callback hell", que torna o código difícil de ler e manter. Nesta aula, entenderemos como callbacks funcionam, os problemas que surgem e como as Promises oferecem um caminho mais elegante.
Assincronismo com callbacks
JavaScript é single-threaded, mas operações assíncronas permitem que o programa não bloqueie enquanto espera por resultados. Callbacks são a base desse modelo. Por exemplo, a função setTimeout recebe um callback que será executado após um atraso:
console.log('Início');
setTimeout(() => {
console.log('Dentro do callback');
}, 1000);
console.log('Fim');
// Saída: Início, Fim, Dentro do callback
Outro exemplo comum é a leitura de arquivos em Node.js:
const fs = require('fs');
fs.readFile('arquivo.txt', 'utf8', (err, data) => {
if (err) {
console.error(err);
return;
}
console.log(data);
});
Callbacks também são usados em eventos do DOM, como cliques, e em funções de ordem superior como forEach e map.
Problemas
O principal problema com callbacks é o aninhamento excessivo, conhecido como "callback hell" ou "pyramid of doom". Isso ocorre quando múltiplas operações assíncronas dependem umas das outras, criando uma estrutura profundamente aninhada:
fs.readFile('a.txt', 'utf8', (err, dataA) => {
if (err) handleError(err);
fs.readFile('b.txt', 'utf8', (err, dataB) => {
if (err) handleError(err);
fs.readFile('c.txt', 'utf8', (err, dataC) => {
if (err) handleError(err);
// ... mais aninhamento
});
});
});
Além da legibilidade prejudicada, o tratamento de erros se torna repetitivo e propenso a erros. Cada callback precisa verificar explicitamente se ocorreu um erro, e erros não tratados podem ser silenciosamente ignorados. Outro problema é a inversão de controle: ao passar um callback para uma função, você perde o controle sobre quando e como ele será chamado, o que pode levar a bugs difíceis de rastrear.
Caminho para Promises
Promises foram introduzidas para resolver os problemas dos callbacks. Uma Promise representa um valor que pode estar disponível agora, no futuro ou nunca, e fornece métodos como .then() e .catch() para encadeamento e tratamento de erros de forma linear. O exemplo anterior com Promises fica mais limpo:
const readFilePromise = (file) => {
return new Promise((resolve, reject) => {
fs.readFile(file, 'utf8', (err, data) => {
if (err) reject(err);
else resolve(data);
});
});
};
readFilePromise('a.txt')
.then((dataA) => readFilePromise('b.txt'))
.then((dataB) => readFilePromise('c.txt'))
.then((dataC) => console.log(dataC))
.catch((err) => console.error(err));
Promises permitem encadeamento, evitando aninhamento, e centralizam o tratamento de erros no .catch(). Embora ainda exista a necessidade de callbacks (por exemplo, no construtor da Promise), o código se torna mais linear e fácil de entender. As Promises também oferecem métodos como Promise.all para paralelismo, que seria complexo com callbacks puros.
Boas práticas
Evite callbacks aninhados sempre que possível. Prefira Promises ou async/await (que veremos em aulas futuras). Ao usar callbacks, mantenha-os rasos e nomeie funções em vez de usar funções anônimas para melhorar a legibilidade. Sempre trate erros adequadamente.
Referências
- MDN: Função de callback
- MDN: Usando Promises
- Node.js: Understanding Callbacks
- JavaScript.info: Callbacks
- JavaScript.info: Promise Basics
- freeCodeCamp: How to Escape Callback Hell
Exercícios
-
Crie uma função
simularOperacaoAsyncque recebe um callback e o executa após 2 segundos usandosetTimeout. Teste chamando-a e imprimindo "Operação concluída".✓ Resposta:function simularOperacaoAsync(callback) { setTimeout(callback, 2000); } simularOperacaoAsync(() => console.log("Operação concluída")); -
Escreva uma função
lerArquivoCallbackque simula a leitura de um arquivo comsetTimeout. Ela deve receber um nome de arquivo e um callback (err, data). Se o nome for "erro.txt", chame o callback com um erro; caso contrário, com os dados "conteúdo do arquivo".✓ Resposta:function lerArquivoCallback(nome, callback) { setTimeout(() => { if (nome === 'erro.txt') { callback(new Error('Arquivo não encontrado'), null); } else { callback(null, 'conteúdo do arquivo'); } }, 1000); } lerArquivoCallback('dados.txt', (err, data) => { if (err) console.error(err); else console.log(data); }); -
Converta o exemplo de callback hell com três leituras de arquivo (a.txt, b.txt, c.txt) para usar Promises. Assuma que
readFilePromisejá está definida.✓ Resposta:readFilePromise('a.txt') .then((dataA) => { console.log(dataA); return readFilePromise('b.txt'); }) .then((dataB) => { console.log(dataB); return readFilePromise('c.txt'); }) .then((dataC) => console.log(dataC)) .catch((err) => console.error(err)); -
Crie uma Promise manual que resolve após 1 segundo com o valor "Pronto!". Use
.then()para imprimir o valor.✓ Resposta:const minhaPromise = new Promise((resolve, reject) => { setTimeout(() => resolve('Pronto!'), 1000); }); minhaPromise.then((valor) => console.log(valor)); -
Explique por que o callback hell é problemático e como as Promises ajudam a resolvê-lo. Dê um exemplo de código com callback hell e a versão com Promises.
✓ Resposta:
O callback hell torna o código difícil de ler e manter devido ao aninhamento profundo, tratamento de erros repetitivo e inversão de controle. Promises permitem encadeamento linear com
.then(), tratamento centralizado de erros com.catch()e melhor legibilidade. Exemplo:// Callback hell asyncOp1(() => { asyncOp2(() => { asyncOp3(() => { // ... }); }); }); // Com Promises asyncOp1() .then(() => asyncOp2()) .then(() => asyncOp3()) .catch((err) => console.error(err));