Callbacks em JavaScript
Em JavaScript, callbacks são um conceito essencial que permite aos desenvolvedores lidar com operações assíncronas de forma eficaz.
Um callback é simplesmente uma função que você passa para outra função como argumento, para que a função receptora possa chamá-la de volta mais tarde. Como funções são valores de primeira classe em JavaScript — podem ser armazenadas em variáveis, passadas adiante e retornadas — qualquer função pode aceitar outra função como parâmetro. Essa única ideia alimenta tudo, desde métodos de array como map até temporizadores e requisições de rede.
Este capítulo aborda o que são callbacks, a diferença entre callbacks síncronos e assíncronos, a convenção error-first, o problema do callback hell que eles criam e como Promises e async/await o resolvem.
Um Primeiro Callback
A função que você passa é o callback; a função que a recebe e invoca é a função de ordem superior. Aqui, um callback é executado após a conclusão de algum trabalho:
finishTask é passado para completeTask e invocado dentro dela. Note que você passa o nome da função (finishTask) sem parênteses — adicionar () a chamaria imediatamente e passaria seu valor de retorno em vez disso.
Callbacks Síncronos vs. Assíncronos
Nem todo callback é sobre espera. Existem dois tipos distintos, e confundi-los é uma fonte comum de bugs.
Callbacks síncronos
Um callback síncrono é executado imediatamente, em ordem, antes que a função externa retorne. Métodos de array são o exemplo clássico:
Aqui, transform é chamado e concluído para cada elemento antes de map retornar. Métodos embutidos como Array.prototype.map, filter, forEach e sort aceitam callbacks síncronos.
Callbacks assíncronos
Um callback assíncrono é entregue a uma operação que será concluída mais tarde — um temporizador, leitura de arquivo ou requisição de rede. A função externa retorna imediatamente, e o callback é disparado assim que o resultado estiver pronto. JavaScript agenda esses callbacks por meio do event loop, que os enfileira e executa quando a pilha de chamadas está vazia.
Mesmo com um atraso de 0ms, o callback é executado depois do código síncrono ao redor. Essa é a característica definidora de um callback assíncrono: ele não pode retornar um valor da maneira normal, portanto a única forma de usar o resultado é colocar seu código de continuação dentro do callback. Para entender exatamente quando eles são executados, veja JavaScript: Event Loop.
A Convenção Error-First
Callbacks assíncronos não podem usar throw para o código que iniciou a operação — quando são executados, aquele código já retornou há muito tempo. A comunidade estabeleceu uma convenção: passar o erro como o primeiro argumento e o resultado como o segundo. O callback sempre verifica o erro primeiro.
Quando tudo ocorre com sucesso, o campo de erro é null. Essa assinatura (err, result) é o padrão nas APIs do Node.js (fs.readFile, dns.lookup e muitos outros).
Callback Hell: A Pirâmide da Perdição
Callbacks funcionam bem para uma única operação. O problema começa quando uma etapa assíncrona depende do resultado da anterior. Cada etapa se aninha dentro da última, e o recuo avança para a direita — a chamada pirâmide da perdição:
Com duas etapas ainda é legível. Adicione uma terceira e quarta — e repita a verificação if (err) em cada camada — e o código se torna difícil de acompanhar, difícil de tratar erros e difícil de alterar. Isso é o callback hell, e é a principal razão pela qual as Promises foram introduzidas.
Boas Práticas para Usar Callbacks
Embora os callbacks sejam poderosos, usá-los excessivamente ou de forma incorreta pode levar ao "callback hell", onde o código fica muito aninhado e difícil de ler e manter. Aqui estão algumas boas práticas para manter seu código limpo:
- Modularize Seu Código: Divida suas funções de callback em funções menores e reutilizáveis. Essa abordagem não só melhora a legibilidade, como também facilita a manutenção do código.
- Trate Erros com Elegância: Sempre trate erros em seus callbacks. Essa prática evita falhas e comportamentos indesejados em ambientes de produção.
- Evite Aninhamento Profundo: Tente achatar suas estruturas de callback ao máximo possível. Ferramentas como async/await ou Promises podem ajudar a gerenciar operações assíncronas de forma mais limpa.
- Cuidado com Closures em Laços: Ao definir callbacks dentro de laços, use
letouconstpara variáveis do laço a fim de evitar bugs relacionados a closures, onde todos os callbacks capturam o valor final do laço.
Indo Além dos Callbacks: Promises e Async/Await
Embora callbacks sejam uma parte fundamental do JavaScript, o JavaScript moderno oferece formas mais abstratas de lidar com código assíncrono, como Promises e async/await.
Usando Promises
Uma Promise representa um valor que pode estar disponível agora, no futuro ou nunca. Em vez de aninhar, etapas dependentes são encadeadas uma após a outra, o que achata a pirâmide e centraliza o tratamento de erros em um único .catch. Comece com JavaScript: Promises, depois veja como as etapas se conectam em JavaScript: Encadeamento de Promises.
Se você tiver uma função antiga baseada em callback (como os exemplos de divide ou getUser acima), pode envolvê-la para que retorne uma Promise — uma técnica chamada promisificação. Veja JavaScript: Promisificação.
Async/Await: Uma Abordagem Mais Limpa
A sintaxe async/await permite escrever código assíncrono que parece código síncrono. É construída sobre Promises e é mais intuitiva do que os padrões tradicionais de callback. Leia JavaScript: Async/Await para ver como você pode usar async/await em vez de callbacks.
Conclusão
Compreender e utilizar callbacks de forma eficaz é crucial para desenvolvedores JavaScript. Seguindo boas práticas e usando recursos modernos como Promises e async/await, você pode escrever código mais limpo e de fácil manutenção. Domine esses conceitos para aprimorar suas habilidades de programação em JavaScript e construir aplicações mais eficientes.