Erros em código assíncrono não funcionam como erros síncronos
No JavaScript síncrono, um erro lançado sobe pela pilha de chamadas até que algum try/catch o capture — ou o programa quebra. Só que o código assíncrono rompe com esse modelo. Quando uma requisição de rede falha, a função que a iniciou já retornou faz tempo. Não existe mais pilha para o erro subir.
As promises resolvem isso dando aos erros um canal próprio. Uma promise pode resolver com um valor ou rejeitar com um motivo. A rejeição é o equivalente assíncrono de lançar um erro. Tudo nesta página gira em torno de uma coisa: garantir que essas rejeições caiam em algum lugar sob seu controle, em vez de simplesmente sumirem.
O try/catch executa e encerra sem problemas. A rejeição acontece 50ms depois, muito tempo após o bloco try já ter terminado. Ninguém pega esse erro. Essa é a armadilha.
try/catch volta a funcionar com await
No momento em que você usa await numa promise, uma rejeição vira um erro lançado dentro da função async. Um try/catch em volta captura esse erro como se fosse um throw síncrono qualquer:
É esse o padrão que você deve buscar primeiro. O await faz a ponte entre o mundo assíncrono e o bom e velho try/catch. Coloque as chamadas await que podem falhar dentro do try e trate os erros no catch.
Um detalhe importante: só a chamada que você de fato aguardou é coberta. Se você disparar uma promise sem usar await, os erros escapam.
O bug mais comum: esquecer do await
Se você chama uma função async sem await (ou sem retornar a promise dela), as rejeições passam despercebidas pelo try/catch ao redor:
O bloco try termina com sucesso. A rejeição acontece no próximo tick, sem ninguém para capturá-la. Você vai ver um aviso de "unhandled promise rejection" no console.
A solução é sempre a mesma: use await na chamada, ou return na promise para que quem chamou possa dar o await.
.catch() é o outro lado da mesma moeda
Dá pra lidar com rejeições sem usar async/await — basta encadear um .catch():
.catch(fn) é um atalho para .then(undefined, fn). Ele captura qualquer rejeição que tenha acontecido antes dele na cadeia. Um .catch() no final da cadeia é o equivalente assíncrono de um try/catch no topo do código — é a sua última linha de defesa antes da rejeição virar "unhandled".
Nada impede misturar os dois estilos. Um padrão bem comum é usar async/await dentro da função e deixar quem chamou anexar o .catch():
fetch não rejeita em erros HTTP
Essa aqui pega todo mundo pelo menos uma vez. O fetch só rejeita em falhas de rede — DNS que não resolveu, conexão recusada, requisição abortada. Uma resposta 404 ou 500 é considerada um fetch bem-sucedido. A promise resolve normalmente; só que resolve com uma response cujo ok é false.
Se você quiser que erros HTTP caiam no seu bloco catch, confira res.ok e lance a exceção manualmente:
Esse tipo de boilerplate vale a pena extrair para um helper assim que você se pegar escrevendo pela segunda vez.
Promise.all falha rápido; Promise.allSettled não
O Promise.all recebe um array de promises e resolve com um array de resultados — a menos que alguma delas rejeite, e nesse caso ele rejeita na hora com esse erro. As outras promises continuam rodando, mas o resultado delas é descartado.
Usar fail-fast faz sentido quando você precisa de todos os resultados e uma única falha já invalida a operação inteira. Agora, quando o que interessa é saber o que aconteceu com cada uma — "tenta esses cinco uploads e me diz quais deram certo e quais falharam" — o certo é usar Promise.allSettled:
allSettled nunca rejeita. Cada item do array vem como {status: "fulfilled", value} ou então {status: "rejected", reason}.
Rethrow e catches específicos
Nem todo erro merece cair no mesmo handler. Um padrão bem comum é capturar, inspecionar e relançar o que você não estava esperando:
Engolir todos os erros com um catch (err) {} vazio esconde bugs de verdade. Só trate o que você consegue tratar de forma útil; o resto, relança.
Unhandled rejections são sua última linha de defesa
Mesmo com código bem escrito, uma hora algo escapa. Tanto o Node.js quanto os navegadores oferecem um hook global para rejeições que ninguém capturou:
// Browser
window.addEventListener("unhandledrejection", event => {
console.error("unhandled:", event.reason);
event.preventDefault(); // suprime o aviso padrão do console
});
// Node.js
process.on("unhandledRejection", reason => {
console.error("unhandled:", reason);
});
Isso não substitui um tratamento adequado — é só um último recurso para log ou telemetria. No Node.js moderno, uma rejeição não tratada derruba o processo por padrão, o que geralmente é o comportamento desejado em produção. Registre o erro, deixe o processo morrer e reinicie do zero.
Checklist prático
Sempre que uma função async encostar em algo que pode falhar, se pergunte:
- Todo
awaitarriscado está dentro de umtry/catch, ou a promise retornada é tratada por quem chamou com.catch()? - Você está de fato dando
awaitna chamada, ou esqueceu e acabou fazendo fire-and-forget? - No caso específico do
fetch, você está checandores.okantes de confiar na resposta? - Ao rodar coisas em paralelo,
Promise.allé a ferramenta certa, ou o caso pedePromise.allSettled? - Existe algum
.catch()no topo da cadeia ou um handler deunhandledrejectionpara nada sumir no silêncio?
Acerte esses cinco pontos e seu código assíncrono para de te surpreender com erros que somem no event loop.
Próximo passo: ES Modules
Com o tratamento de erros em código assíncrono, fechamos o capítulo de async. A partir daqui, vamos ver como o JavaScript organiza código entre arquivos — import, export e o sistema de módulos que sustenta qualquer projeto moderno.
Perguntas frequentes
Como tratar erros dentro de uma função async?
Envolva as chamadas com await em um bloco try/catch. Qualquer rejeição de uma promise aguardada vira um erro lançado que o catch consegue capturar. Outra opção é deixar o erro subir e tratá-lo no ponto de chamada, usando .catch() na promise retornada.
Por que meu try/catch não está capturando o erro?
Normalmente é porque o erro acontece em um trecho que não foi aguardado. Se você chama uma função async sem await (ou sem retornar a promise), a rejeição escapa do try/catch em volta. Sempre use await ou return na promise da qual você quer capturar erros.
O que acontece quando uma promise rejeita e ninguém captura?
Rola uma unhandled rejection. No Node.js, o evento unhandledRejection é disparado e, nas versões mais recentes, o processo é encerrado por padrão. No navegador, dispara o window.onunhandledrejection e sobe um warning no console. Em qualquer caso, resolva com um .catch() ou com try/catch em volta de um await.
Como o Promise.all se comporta quando algo falha?
O Promise.all rejeita assim que qualquer uma das promises da lista falha — as outras continuam executando, mas os resultados são descartados. Se você precisa do resultado de todas, independentemente de falhas, use Promise.allSettled: ele resolve com um array de {status, value} ou {status, reason}.