Uma chamada fetch pode ficar esperando uma dependência lenta enquanto o restante da aplicação aguarda a Promise. Se o seu serviço precisa responder em um prazo conhecido, coloque esse prazo no sinal enviado ao fetch. Não esconda o timer em uma Promise paralela que apenas abandona o resultado.

No Node.js, AbortSignal.timeout(milliseconds) cria um sinal que será interrompido depois do intervalo. Passe-o em signal e mantenha a mesma fronteira até consumir o corpo da resposta. Se o chamador também puder cancelar, combine o sinal manual com o prazo usando AbortSignal.any().

O código deste artigo usa TypeScript e APIs nativas. Eu verifiquei a lógica equivalente com um servidor HTTP local que atrasa cabeçalhos e corpo, sem medir uma aplicação de produção. A documentação atual do Node.js sobre objetos globais, consultada em 28/09/2026, registra os métodos e suas versões de entrada. A referência da MDN para AbortSignal.timeout(), consultada na mesma data, documenta TimeoutError, AbortSignal.any() e a limitação de que o timeout não tem cancelamento próprio.

Diagrama mostra um fetch do Node.js sendo interrompido por um prazo com AbortSignal.timeout.

Resposta curta

  • Passe AbortSignal.timeout(prazo) na opção signal do fetch.
  • Combine o prazo com o cancelamento do chamador usando AbortSignal.any().
  • Diferencie timeout, cancelamento manual, erro de rede e status HTTP.
  • Não trate o abort do cliente como rollback de uma operação que já chegou ao servidor.

fetch tem timeout próprio no Node.js?

Não confunda o tempo que a sua aplicação aceita esperar com um limite interno do transporte. A forma explícita e portátil de controlar uma chamada é passar um AbortSignal; a documentação do Node.js lista AbortSignal.timeout(delay) como o método que cria um sinal interrompido após o atraso informado.

Para uma chamada simples, o código é curto:

async function loadProfile(url: string): Promise<unknown> {
  const response = await fetch(url, {
    signal: AbortSignal.timeout(5_000),
  });

  if (!response.ok) {
    throw new Error(`Serviço respondeu HTTP ${response.status}`);
  }

  return response.json();
}

Esse prazo cobre a operação enquanto o sinal continua ligado ao fetch. O response.json() também fica dentro da mesma operação assíncrona. Se a resposta demorar além do limite, a Promise rejeita e o chamador precisa decidir como registrar, responder ou tentar novamente.

O valor 5_000 é apenas um exemplo de configuração. O prazo correto depende do contrato da rota e da dependência. Não há um número universal que transforme uma chamada lenta em saudável.

Como combinar timeout e cancelamento manual?

Use dois sinais quando há dois motivos legítimos para interromper a chamada: o prazo da operação e uma decisão do chamador. AbortSignal.any() retorna um novo sinal que aborta quando qualquer sinal da lista aborta, preservando a razão do sinal que venceu, segundo a documentação do Node.js.

type RequestOptions = {
  signal?: AbortSignal;
  timeoutMs: number;
};

function requestSignal({ signal, timeoutMs }: RequestOptions): AbortSignal {
  const deadline = AbortSignal.timeout(timeoutMs);

  return signal
    ? AbortSignal.any([signal, deadline])
    : deadline;
}

async function loadJson<T>(
  url: string,
  options: RequestOptions,
): Promise<T> {
  const deadline = AbortSignal.timeout(options.timeoutMs);
  const request = options.signal
    ? AbortSignal.any([options.signal, deadline])
    : deadline;

  const response = await fetch(url, { signal: request });

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

  return (await response.json()) as T;
}

O helper requestSignal mostra a regra, mas o exemplo loadJson mantém o sinal de prazo disponível para classificar a falha. Em um projeto real, evite criar o prazo duas vezes. Extraia um objeto com deadline e request para que o código de tratamento observe exatamente o mesmo sinal que foi enviado ao fetch.

O AbortSignal.any() entrou no Node.js em versões mais recentes que AbortSignal.timeout(). Se a sua matriz de suporte inclui runtimes antigos, confira a versão documentada antes de publicar esse código ou use um AbortController com setTimeout como fallback controlado.

Como saber se foi timeout, cancelamento ou erro de rede?

O tipo do erro sozinho nem sempre explica a decisão que interrompeu o trabalho. Guarde o sinal do prazo e o sinal do chamador. Depois do catch, verifique qual deles abortou antes de classificar a falha para logs, métricas ou política de retry.

type FailureKind = "timeout" | "cancelled" | "network";

function classifyFailure(
  error: unknown,
  deadline: AbortSignal,
  callerSignal?: AbortSignal,
): FailureKind {
  if (deadline.aborted) {
    return "timeout";
  }

  if (callerSignal?.aborted) {
    return "cancelled";
  }

  if (error instanceof Error) {
    return "network";
  }

  return "network";
}

Em um timeout nativo, a razão do sinal é um DOMException com nome TimeoutError, conforme a MDN. Um cancelamento manual pode usar AbortController.abort(reason) e carregar uma razão própria. Por isso, observar o estado dos sinais também é útil quando vários ambientes de fetch produzem objetos de erro diferentes.

O status HTTP é outro caminho. Um 404 ou 503 normalmente resolve a Promise de fetch; ele não é automaticamente uma falha de rede. Teste response.ok e transforme o status em um erro do domínio antes de aplicar uma política de retry.

O ponto de decisão não é o nome do erro. É a fronteira que você controlou. Um timeout conhecido pode ser tratado como prazo excedido, enquanto uma conexão recusada pode merecer outra ação. Misturar os dois caminhos faz uma política de retry parecer simples e ficar errada no primeiro incidente.

O timeout também limita a leitura do corpo?

Sim, se o mesmo sinal continuar associado ao fetch enquanto o corpo é consumido. A documentação da MDN demonstra o padrão envolvendo a chamada e a leitura da resposta na mesma operação. Isso importa para respostas que enviam cabeçalhos cedo, mas demoram para produzir todos os bytes.

async function readText(url: string, timeoutMs: number): Promise<string> {
  const signal = AbortSignal.timeout(timeoutMs);
  const response = await fetch(url, { signal });

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

  return response.text();
}

Não coloque um timer apenas ao redor da primeira await fetch(url). Essa primeira Promise pode resolver quando os cabeçalhos chegam. A leitura de text(), json() ou de um stream ainda pode estar trabalhando. O sinal deve acompanhar a fronteira que você quer limitar.

Se o prazo precisa ser encerrado imediatamente quando a operação termina, AbortSignal.timeout() não oferece um método para cancelar o próprio timer. Para rotinas com muitos listeners ou prazos longos, a MDN recomenda considerar um AbortController, setTimeout e clearTimeout, com a limpeza no finally.

Como testar um fetch que expira sem esperar na rede?

Teste o comportamento com um servidor HTTP local que você controla. O teste abaixo é uma ilustração executável: crie uma rota que envia os cabeçalhos, espera e só depois envia o corpo. Assim, você verifica tanto o timeout do pedido quanto o timeout durante a leitura.

import { createServer } from "node:http";

const server = createServer((_request, response) => {
  response.writeHead(200, { "content-type": "text/plain" });
  response.flushHeaders();

  setTimeout(() => {
    response.end("resposta tardia");
  }, 200);
});

await new Promise<void>((resolve) => server.listen(0, resolve));
const address = server.address();

if (!address || typeof address === "string") {
  throw new Error("Servidor não abriu uma porta");
}

try {
  const response = await fetch(`http://127.0.0.1:${address.port}`, {
    signal: AbortSignal.timeout(30),
  });

  await response.text();
  throw new Error("O timeout deveria interromper a leitura");
} catch (error) {
  if (error instanceof DOMException && error.name === "TimeoutError") {
    console.log("timeout verificado");
  } else {
    throw error;
  }
} finally {
  server.close();
}

No repositório de produção, transforme essa ideia em um teste do runner usado pela equipe. Cubra também a resposta rápida, o cancelamento manual e um erro de conexão. Não dependa de sleep contra uma API pública: a rede externa torna o teste lento e deixa a causa da falha ambígua.

Eu executei a lógica equivalente com um servidor local atrasado no Node.js disponível no ambiente de trabalho. Esse teste confirma o caminho de timeout e a leitura do corpo. Ele não prova que todo proxy, biblioteca de fetch ou servidor interrompa o trabalho remoto da mesma forma.

Posso repetir automaticamente depois de um timeout?

Só depois de decidir se a operação é segura para repetir. O cliente pode abortar a espera depois que o servidor recebeu a requisição. Isso vale para pagamentos, criação de registros, geração de relatórios e qualquer endpoint que produza efeito externo. Interromper fetch não desfaz uma transação remota.

Para uma leitura idempotente, um retry limitado pode fazer sentido depois de classificar o timeout. Para uma escrita, use uma chave de idempotência ou um contrato explícito do servidor antes de repetir. A guia sobre testar idempotência de API ajuda a verificar a parte de proteção contra efeitos duplicados.

O guia para testar lógica de retry em JavaScript sem esperar trata o relógio do teste. Ele não substitui a decisão de produto sobre repetir uma chamada que talvez já tenha chegado ao destino.

O que AbortSignal.timeout() não resolve?

O sinal informa que uma operação deve parar. Ele não torna a dependência rápida, não corrige um status HTTP, não valida JSON e não desfaz trabalho no servidor. Depois do timeout, ainda pode existir uma operação remota em andamento, dependendo de quando o servidor recebeu e processou a requisição.

Também não use um cast TypeScript para esconder a resposta. Depois que a chamada passar pela fronteira de tempo e transporte, valide o payload antes de tratá-lo como dado confiável. Veja como validar JSON externo antes de usá-lo em TypeScript quando a próxima camada precisa de um contrato de formato.

Perguntas frequentes

fetch rejeita a Promise para um status 404?

Não necessariamente. Um status HTTP pode produzir uma Response resolvida. Verifique response.ok ou response.status e transforme a resposta em uma falha do seu domínio. O timeout é uma interrupção do sinal, enquanto 404 é uma resposta recebida do servidor.

AbortSignal.timeout() substitui AbortController?

Para um prazo simples, ele evita criar e limpar um timer manual. Você ainda precisa de AbortController quando o chamador pode cancelar, quando quer uma razão própria ou quando precisa encerrar o timer explicitamente. Combine os sinais quando prazo e cancelamento representam decisões diferentes.

Um timeout impede que o servidor termine o trabalho?

Não. Ele interrompe a espera e o processamento observável pelo cliente. A requisição pode ter chegado ao servidor antes do abort. Para efeitos externos, use idempotência, confirmação de estado ou um mecanismo de cancelamento que o próprio servidor entenda.

Conclusão

O padrão básico é fetch(url, { signal: AbortSignal.timeout(ms) }). Quando existe cancelamento manual, una os sinais com AbortSignal.any(). Quando a resposta chega em partes, mantenha o sinal ligado também à leitura do corpo. E antes de repetir, classifique a falha e confirme se o efeito remoto é idempotente.

O artigo não define um prazo universal nem substitui a política do seu serviço. Ele oferece uma fronteira explícita para que o restante da aplicação consiga decidir o que fazer quando uma dependência demora mais do que o contrato permite.

Fontes consultadas