TypeError: Failed to fetch é o navegador a dizer que uma chamada a fetch() nunca chegou ao fim. Não que tenha falhado - que nunca foi longe o suficiente para ter sequer um resultado.

Esta distinção importa mais do que tudo o resto nesta página, porque afasta quase tudo aquilo de que se suspeita primeiro. Um 404 não produz isto. Um 500 também não. Esses são pedidos concluídos: o servidor respondeu, fetch() resolve-se e response.ok é false. Failed to fetch quer dizer que não há qualquer objeto de resposta para inspecionar, que é exatamente a razão por que a mensagem lhe diz tão pouco.

O Chrome, o Firefox e o Safari escrevem-no cada um à sua maneira - Failed to fetch, NetworkError when attempting to fetch resource, Load failed - e querem dizer o mesmo.

As causas, pela ordem por que acontecem na realidade

CORS. A mais comum de longe. O pedido saiu, o servidor respondeu e o navegador recusou-se a entregar a resposta ao seu código porque os cabeçalhos não o permitiam. A consola imprime uma mensagem à parte sobre a política de origem, e o fetch é rejeitado com o erro genérico. Se ler apenas a rejeição não fica a saber nada; a linha acima dela é que é a resposta.

A ligação nunca chegou a acontecer. Sem rede, falha de DNS, anfitrião inalcançável, servidor que não está à escuta. Este é o mesmo terreno da família ERR_CONNECTION, e o painel de rede dá-lhe o nome.

Alguma coisa o bloqueou. Um bloqueador de anúncios, uma extensão de privacidade, um proxy da empresa. Os pedidos para tudo o que se pareça com análises ou rastreio são recusados antes de saírem do navegador, e o painel de rede marca-os como bloqueados. Este é invisível na sua própria máquina se for o único sem essa extensão.

Conteúdo misto. Uma página https a pedir um URL http. O navegador bloqueia-o sem mais.

O pedido foi cancelado. A página navegou para outro lado, um componente foi desmontado, disparou um AbortController, o separador fechou-se a meio do pedido. Coisa banal numa aplicação de página única, e no registo de erros aparece como uma enxurrada de falhas que não aconteceram a ninguém.

Um URL errado. Uma gralha, um caminho relativo que resolve para onde não devia, uma variável de ambiente que estava vazia na altura da compilação, pelo que o pedido foi para undefined/api/thing.

Failed to fetch dynamically imported module

Uma variante concreta que merece o seu próprio parágrafo, porque a causa não está no seu código e a correção também não.

Uma aplicação construída com divisão de código carrega blocos com nomes de ficheiro com hash. Faz o deploy, os hashes mudam, os ficheiros antigos deixam de existir. Quem já tinha a página aberta - um separador deixado aberto a noite toda, um telemóvel que nunca recarregou - pede um nome de ficheiro que agora é um 404, e recebe este erro no momento em que navega para uma rota cujo bloco ainda não foi carregado.

Duas correções, e funcionam em conjunto. Mantenha durante algum tempo os recursos da compilação anterior em vez de os substituir, para que os separadores abertos continuem a funcionar. E apanhe a falha no encaminhador: se uma importação dinâmica for rejeitada, ofereça um recarregamento, porque um recarregamento resolve mesmo o problema.

Como descobrir qual delas tem

A consola não lho vai dizer. O painel de rede vai, e demora uns dez segundos.

Abra-o, reproduza a falha e encontre o pedido a vermelho. Depois leia três colunas:

  • Estado. Vazio ou (failed) quer dizer que o pedido nunca chegou ao fim. (blocked:...) dá o nome daquilo que o bloqueou.
  • O anfitrião. O seu próprio domínio, ou o de outra pessoa. Uma falha contra um terceiro é um problema diferente de uma falha contra a sua própria API.
  • A mensagem de consola mesmo acima da rejeição. Se falar de uma origem ou de um preflight, tem um problema de CORS e todo o resto desta página é distração.

Se o pedido nem sequer aparecer no painel de rede, nunca saiu do navegador: conteúdo misto, uma extensão, ou um URL que nunca foi válido.

Session Replay

Extensão gratuita do Chrome. Um clique na página que se está a portar mal captura a captura de ecrã, a consola e o registo de rede, e devolve-lhe um link para colar no ticket.

Instalar a extensão

Porque é o pior erro para receber em segunda mão

Tudo o que está acima parte do princípio de que consegue reproduzi-lo. Na maior parte das vezes não consegue, porque as causas que produzem este erro são as que vivem na máquina de outra pessoa: a extensão dela, a rede dela, o proxy da empresa dela, o separador velho dela.

Por isso chega como “a página só diz que correu alguma coisa mal”, e a mensagem de consola que lhe poderão citar, Failed to fetch, é a única mensagem da página que não traz informação nenhuma. A prova que resolveria a questão é uma linha num painel de rede que ninguém está a ver.

É esse o argumento para capturar o registo de rede no momento em que acontece, em vez de fazer perguntas depois. Um ficheiro HAR é a maneira manual de o fazer. De um modo ou de outro, o que resolve esta classe de erro é a linha, não a frase.

A versão curta

  • Quer dizer que o pedido nunca chegou ao fim. Sem resposta, sem código de estado, nada para inspecionar.
  • Um 404 ou um 500 não o causam - esses são percursos de ida e volta bem sucedidos com respostas infelizes.
  • Veja primeiro a linha de consola acima da rejeição. Se falar de CORS, é essa a sua resposta.
  • Depois verifique se o pedido sequer aparece no painel de rede. Se não aparecer, alguma coisa no navegador o travou antes de sair.
  • Depois de um deploy, “failed to fetch dynamically imported module” quer dizer que um separador antigo está a pedir um ficheiro que a sua última compilação apagou.