Menu

Tratamento de erros em funções async no JavaScript

Entenda como os erros realmente se propagam em código assíncrono: try/catch com async/await, .catch em promises e as armadilhas que engolem falhas em silêncio.

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.

index.js
Output
Click Run to see the output here.

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:

index.js
Output
Click Run to see the output here.

É 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:

index.js
Output
Click Run to see the output here.

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.

index.js
Output
Click Run to see the output here.

.catch() é o outro lado da mesma moeda

Dá pra lidar com rejeições sem usar async/await — basta encadear um .catch():

index.js
Output
Click Run to see the output here.

.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():

index.js
Output
Click Run to see the output here.

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.

index.js
Output
Click Run to see the output here.

Se você quiser que erros HTTP caiam no seu bloco catch, confira res.ok e lance a exceção manualmente:

index.js
Output
Click Run to see the output here.

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.

index.js
Output
Click Run to see the output here.

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:

index.js
Output
Click Run to see the output here.

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:

index.js
Output
Click Run to see the output here.

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 await arriscado está dentro de um try/catch, ou a promise retornada é tratada por quem chamou com .catch()?
  • Você está de fato dando await na chamada, ou esqueceu e acabou fazendo fire-and-forget?
  • No caso específico do fetch, você está checando res.ok antes de confiar na resposta?
  • Ao rodar coisas em paralelo, Promise.all é a ferramenta certa, ou o caso pede Promise.allSettled?
  • Existe algum .catch() no topo da cadeia ou um handler de unhandledrejection para 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}.

Aprenda a programar com o Coddy

COMEÇAR