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.js para gerenciar fluxos complexos de callbacks em projetos legados.

Referências

Exercícios

  1. Escreva uma função lerArquivo que simula a leitura de um arquivo usando setTimeout e um callback. O callback deve receber (erro, dados). Teste com sucesso e erro.
  2. ✓ Resposta:
    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);
    });
    
  3. Refatore o código abaixo, que está em callback hell, para usar Promises. Assuma que getUser, getPosts e getComments retornam Promises.
  4. 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);
            });
          }
        });
      }
    });
    

    ✓ Resposta:
    getUser(id)
      .then(user => getPosts(user.id))
      .then(posts => getComments(posts[0].id))
      .then(comments => console.log(comments))
      .catch(err => console.error(err));
    
  5. Crie uma função simularRequisicao que retorna uma Promise. A Promise deve resolver após 2 segundos com um objeto { dados: 'exemplo' }. Use setTimeout para simular o atraso.
  6. ✓ Resposta:
    function simularRequisicao() {
      return new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve({ dados: 'exemplo' });
        }, 2000);
      });
    }
    
    simularRequisicao().then(resultado => console.log(resultado));
    
  7. Explique por que o código abaixo é um exemplo de callback hell e como você poderia melhorá-lo.
  8. step1(function(err, result1) {
      step2(function(err, result2) {
        step3(function(err, result3) {
          console.log(result3);
        });
      });
    });
    

    ✓ Resposta:

    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.

  9. 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.
  10. ✓ Resposta:
    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']