Envolva uma chamada fetch() em um try/catch, acerte um endpoint que responde com 404, e o bloco catch nunca executa. A promise se resolve. res.json() analisa qualquer corpo que o servidor tenha devolvido, muitas vezes um payload de erro como {"error": "user not found"}, e o resto da função trata isso como o registro que pediu. Nenhuma exceção, nenhum aviso no console. O dado errado segue adiante até algo mais quebrar por causa dele.
| |
Esse catch só dispara para uma requisição que nunca se completa. Tudo o resto precisa de uma verificação que você mesmo tem que escrever.
Por que fetch() trata 404 e 500 como sucesso?
fetch() é rejeitada quando a requisição em si falha: a resolução DNS falha, a conexão cai no meio da transferência, uma política CORS bloqueia a resposta antes que ela chegue ao seu código, a rede do usuário desaparece. Em cada um desses casos, nenhuma resposta do servidor chegou até você.
Um 404 ou um 500 é um servidor respondendo. A conexão funcionou e voltou uma resposta com headers e corpo. A troca se completou; se completou com uma má notícia. A especificação Fetch conta isso como um fetch bem-sucedido e deixa o código de status para você inspecionar. response.ok é true para qualquer status na faixa 200–299 e false para todo o resto, incluindo respostas 4xx/5xx e redirecionamentos que o fetch não seguiu.
Esse é o detalhe que pega quem vem do Axios, que por padrão rejeita qualquer status não-2xx. Nenhum dos dois comportamentos está errado. A Fetch API só rejeita por falhas no nível de transporte, com a lógica de que um 404 não é algo excepcional na web. Mas se você espera a semântica do Axios de fetch(), acaba lançando o bug acima em produção.
Como verificar response.ok e lançar seu próprio erro?
Um if, posicionado antes de tocar no corpo, resolve:
| |
Isso já basta para um script. Com mais de um ponto de chamada, repetir essas linhas toda vez é como, mais cedo ou mais tarde, uma delas acaba esquecida. Encapsule isso de uma vez:
| |
Agora todo chamador recebe uma exceção de verdade em um status ruim, com o código de status e o corpo da resposta do servidor anexados.
Como ler o corpo do erro quando ele não é JSON?
Um 500 da sua própria API provavelmente volta como JSON. Um 502 de um reverse proxy ou uma página de bloqueio de um WAF geralmente volta como HTML. Chame res.json() sobre isso e você recebe um SyntaxError (“Unexpected token ‘<’”) que não tem nada a ver com a falha real e te manda depurar a coisa errada.
O fetchJSON acima evita isso lendo o corpo do erro com res.text(), que não se importa se o conteúdo é JSON válido. Se você quiser o objeto analisado quando ele é JSON, verifique o content type antes:
| |
O corpo de uma Response só pode ser lido uma vez. Se você precisa do texto bruto para log e do JSON analisado para o chamador, chame res.clone() primeiro e leia uma cópia de cada. Ler o original duas vezes lança “body stream already read.”
Como diferenciar uma falha de rede de um erro HTTP?
Uma vez que fetchJSON lança exceção nos dois casos, um chamador que queira reagir de forma diferente precisa verificar qual dos dois recebeu:
| |
Uma falha de rede aparece como um simples TypeError sem código de status, porque nunca houve uma resposta de onde tirar um. A mensagem varia por navegador: “Failed to fetch” no Chrome, “NetworkError when attempting to fetch resource” no Firefox. Então diferencie pelo tipo, não pela mensagem.
Há um caso que parece falha de rede, mas não é. Se uma requisição cross-origin falha com esse TypeError enquanto os logs do servidor mostram que ela chegou e respondeu 200, o navegador bloqueou a resposta antes que seu código a visse. A correção está nos headers CORS do servidor, não no tratamento de erros do cliente; /pt/posts/web/o-que-e-cors-como-corrigir-erros-cors/ explica o que esses headers precisam dizer.
Como adicionar um timeout ao fetch()?
fetch() não tem timeout. Ele espera o tempo que a conexão ficar aberta, então um backend que trava em vez de retornar um erro mantém a requisição pendente indefinidamente. AbortSignal.timeout() corta isso:
| |
Depois de 5 segundos a requisição é abortada e a promise é rejeitada com um TimeoutError, diferente do AbortError que você recebe ao cancelar uma requisição manualmente:
| |
AbortSignal.timeout() alcançou disponibilidade Baseline, ou seja, suporte em todos os navegadores principais atuais, em 2024. Para um navegador ou runtime mais antigo, construa a mesma coisa com setTimeout e um AbortController:
| |
Esse fallback rejeita com AbortError, não TimeoutError, então uma verificação como a de cima precisa aceitar os dois nomes.
Se o chamador também precisa cancelar por motivos próprios (o usuário fechou o diálogo, uma requisição mais recente substituiu essa), combine os dois sinais com AbortSignal.any([ownSignal, timeoutSignal]) em vez de escolher um só. AbortSignal.any() é mais recente que timeout(), então confirme que seus runtimes-alvo o suportam.
fetch vs Axios: quem lança erro em um 404?
fetch() | Axios | |
|---|---|---|
| Resposta 4xx / 5xx | Se resolve; verifique response.ok | É rejeitada; cai no catch |
| Corpo da resposta | Chame .json() / .text() você mesmo | Analisado automaticamente em response.data |
| Timeout da requisição | Nenhum embutido; use AbortSignal.timeout() | Opção de configuração timeout |
| Falha de rede / CORS | Rejeita com TypeError | Rejeita com um AxiosError |
fetch() vem embutido em todo runtime moderno, então não há nada para instalar. Axios já vem com erro automático em status de falha e parsing automático de JSON, ao custo de uma dependência. Se o código já usa Axios para interceptors ou cancelamento, use os padrões dele. Se não usa, o wrapper fetchJSON acima dá o mesmo comportamento em cerca de vinte linhas.
Quais erros deixam falhas de fetch sem tratamento?
- Verificar
response.okdepois de chamar.json(). A essa altura o corpo já foi consumido, e em uma página de erro HTML o parsing já lançou uma exceção. Verifiqueokprimeiro, leia o corpo depois. - Chamar uma função assíncrona de um event handler sem
awaitou.catch().onClick={() => fetchJSON(url)}perde a rejeição: nenhum bloco catch executa, e dependendo do runtime você recebe um aviso de unhandled rejection ou nada. /pt/posts/javascript/async-await-e-event-loop-javascript/ explica para onde vai essa promise rejeitada. - Um
catchque loga e retornaundefined.catch (err) { console.error(err); }engole o erro. O chamador recebeundefinedonde esperava dados e quebra em outro lugar, mais longe da causa real. - Assumir que todo corpo não-2xx é JSON. Essa é a causa mais comum de “meu error handler lançou um erro diferente”. Leia com
.text()ou verifique ocontent-typeantes.
Onde a verificação de response.ok deveria viver?
Em um único módulo. fetch() faz do silêncio o comportamento padrão, então toda chamada sem uma verificação explícita de status reproduz o bug do início deste artigo. Escreva fetchJSON e HttpError uma vez, adicione o tratamento de timeout e content-type vistos acima, e torne isso a única forma de sua aplicação falar com um servidor. Uma regra de lint ou um hábito de code review que sinalize chamadas fetch() diretas fora desse módulo mantém essa disciplina.