Pular para o conteúdo

Fetch com timeout usando AbortSignal.timeout: pare de deixar requisição pendurada

Um dos jeitos mais discretos de deixar uma interface ruim é fazer uma requisição e agir como se ela fosse voltar em um tempo razoável. A API caiu? A rede está oscilando? O servidor resolveu tirar um cochilo? Sem timeout, sua tela pode ficar esperando para sempre, com o usuário encarando um loading que já virou parte da decoração.

A boa notícia é que JavaScript moderno tem uma forma bem mais limpa de lidar com isso: AbortSignal.timeout(). Em vez de criar um setTimeout manual, guardar referência, chamar clearTimeout e torcer para não vazar estado em algum canto, você pode passar um signal direto para o fetch.

O problema

Por padrão, fetch não recebe uma opção simples chamada timeout. Isso significa que, se você não controlar o cancelamento, a requisição pode ficar pendurada por tempo demais. Em alguns casos, o navegador ou a rede eventualmente resolvem. Em outros, a experiência do usuário simplesmente fica travada em um estado ruim.

Um exemplo comum é este:

async function loadPosts() {
  const response = await fetch("/api/posts");
  const posts = await response.json();

  return posts;
}

Esse código é limpo, mas otimista demais. Ele assume que a API vai responder, que a rede vai colaborar e que o usuário está disposto a esperar. Na vida real, pelo menos uma dessas coisas costuma falhar no pior momento possível.

Usando AbortSignal.timeout

O AbortSignal.timeout() cria um signal que aborta automaticamente depois do tempo informado em milissegundos. A MDN descreve esse método como uma forma de retornar um AbortSignal que será abortado depois de determinado período.

async function loadPosts() {
  const response = await fetch("/api/posts", {
    signal: AbortSignal.timeout(5000),
  });

  if (!response.ok) {
    throw new Error("Erro ao carregar posts");
  }

  return response.json();
}

Agora a requisição tem um limite de 5 segundos. Se a API não responder nesse intervalo, o signal aborta a operação. Isso não resolve todos os problemas de rede do universo, mas pelo menos impede que a interface fique esperando eternamente como se paciência fosse requisito funcional.

Tratando timeout de forma decente

O próximo passo é tratar o erro. Quando o timeout acontece, a operação pode lançar um erro com nome TimeoutError. Também podem existir outros cenários de abort, como navegação, ação do usuário ou falta de suporte no ambiente.

async function fetchWithTimeout(url, options = {}) {
  try {
    const response = await fetch(url, {
      ...options,
      signal: AbortSignal.timeout(5000),
    });

    if (!response.ok) {
      throw new Error(`Erro HTTP: ${response.status}`);
    }

    return response.json();
  } catch (error) {
    if (error.name === "TimeoutError") {
      throw new Error("A requisição demorou demais para responder.");
    }

    if (error.name === "AbortError") {
      throw new Error("A requisição foi cancelada.");
    }

    throw error;
  }
}

Esse wrapper já melhora bastante a vida em telas que dependem de API. Em vez de espalhar tratamento de timeout por todos os componentes, você centraliza uma decisão mínima de rede. O componente pode então mostrar uma mensagem mais honesta, permitir nova tentativa ou seguir para um estado alternativo.

Um caso de uso real

Imagine uma busca de posts em um painel administrativo. Se a API demora demais, talvez seja melhor informar o usuário e liberar uma nova tentativa do que deixar um spinner rodando sem contexto.

async function searchPosts(query) {
  const params = new URLSearchParams({ q: query });

  return fetchWithTimeout(`/api/posts?${params.toString()}`);
}

try {
  const posts = await searchPosts("react");
  console.log(posts);
} catch (error) {
  console.error(error.message);
}

Esse exemplo também usa URLSearchParams para montar a query string sem concatenar parâmetro na mão. Pequenas decisões assim reduzem erro bobo, especialmente quando o valor vem de input do usuário.

Cuidados importantes

O AbortSignal.timeout() aparece como Baseline 2024 na MDN, mas ainda vale verificar compatibilidade se você precisa dar suporte a navegadores antigos, WebViews defasadas ou ambientes específicos. Para projetos modernos, a API já é uma opção bem mais agradável do que montar timeout manual em toda chamada.

Outro ponto: timeout não é retry. Se você quer tentar de novo automaticamente, isso precisa ser uma decisão separada. Nem toda requisição deveria ser repetida, especialmente operações que alteram estado, criam pagamento, enviam formulário ou disparam algum efeito importante no backend.

Timeout bom não é só cancelar rápido. É cancelar com intenção e responder ao usuário de um jeito menos preguiçoso.

Conclusão

Adicionar timeout ao fetch é uma melhoria pequena que evita muita experiência ruim. Com AbortSignal.timeout(), o código fica mais direto, menos cheio de gambiarras e mais fácil de padronizar em um projeto moderno.

Não é uma daquelas técnicas que viram post viral. Mas é exatamente o tipo de detalhe que separa uma interface “funciona na minha máquina” de uma interface que respeita o tempo de quem está usando.

Fontes

"Loop"

"Loop"

Lucas “Loop” Torres é desenvolvedor web e escreve sobre JavaScript, TypeScript, PHP e as pequenas decisões que fazem um projeto sair do “funciona na minha máquina” para algo minimamente decente. Ganhou o apelido na faculdade depois de alguns loops infinitos memoráveis e, desde então, tenta transformar bugs, ferramentas e tendências da web em conteúdo útil para quem também vive entre commits, café e deploys de última hora.View Author posts