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

Exercícios

  1. Crie uma função simularOperacaoAsync que recebe um callback e o executa após 2 segundos usando setTimeout. Teste chamando-a e imprimindo "Operação concluída".

    ✓ Resposta:
    function simularOperacaoAsync(callback) {
      setTimeout(callback, 2000);
    }
    
    simularOperacaoAsync(() => console.log("Operação concluída"));
  2. Escreva uma função lerArquivoCallback que simula a leitura de um arquivo com setTimeout. 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);
    });
  3. Converta o exemplo de callback hell com três leituras de arquivo (a.txt, b.txt, c.txt) para usar Promises. Assuma que readFilePromise já 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));
  4. 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));
  5. 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));