AbortController oferece um padrão comum para cancelar operações assíncronas no Node.js. Ele pode interromper Fetch, timers, streams, processos filhos e várias APIs que aceitam AbortSignal. O objetivo não é apenas reduzir latência: cancelamento libera sockets, arquivos, memória e capacidade de serviços dependentes.
Sem cancelamento, uma requisição que já expirou pode continuar consultando banco, chamando APIs e processando dados. Em picos, esse trabalho inútil aumenta a sobrecarga e pode criar uma cascata de timeouts.
Conceitos básicos
const controller = new AbortController();
const { signal } = controller;
signal.addEventListener('abort', () => {
console.log('Operação cancelada', signal.reason);
});
controller.abort(new Error('Cancelamento solicitado'));O controller emite o cancelamento; o signal é passado para as operações. Um signal só muda do estado não abortado para abortado e não pode ser reutilizado para uma nova tentativa.
Verificando o estado
if (signal.aborted) {
throw signal.reason;
}
signal.throwIfAborted();throwIfAborted é útil antes de iniciar uma etapa cara e dentro de loops cooperativos.
Timeout com AbortSignal
const signal = AbortSignal.timeout(5_000);
const response = await fetch('https://api.example.com/items', {
signal,
});O timeout cancela a operação depois do intervalo. Trate o erro sem confundir timeout, cancelamento do cliente e falha de rede.
Fetch com timeout e tratamento
async function fetchJson(url, timeoutMs = 5000) {
const signal = AbortSignal.timeout(timeoutMs);
try {
const response = await fetch(url, { signal });
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
if (signal.aborted) {
throw new Error(`Timeout após ${timeoutMs} ms`, { cause: error });
}
throw error;
}
}Defina também limites para o corpo. Um servidor pode responder headers rapidamente e enviar dados indefinidamente.
Combinando sinais
Uma operação pode ser cancelada pelo timeout ou pelo encerramento da requisição:
const timeoutSignal = AbortSignal.timeout(10_000);
const signal = AbortSignal.any([
requestSignal,
timeoutSignal,
]);
await fetch(url, { signal });Confirme a versão mínima do Node.js usada pelo projeto. Quando AbortSignal.any não estiver disponível, crie um controller e propague eventos manualmente.
Cancelamento em servidores HTTP
app.get('/report', async (req, res, next) => {
const controller = new AbortController();
req.on('close', () => {
if (!res.writableEnded) {
controller.abort(new Error('Cliente desconectou'));
}
});
try {
const report = await buildReport({ signal: controller.signal });
res.json(report);
} catch (error) {
if (controller.signal.aborted) return;
next(error);
}
});O evento exato e a condição de desconexão dependem do framework. Teste requests concluídas normalmente para não abortar após a resposta válida.
Propagando o signal
async function buildReport({ signal }) {
signal.throwIfAborted();
const users = await loadUsers({ signal });
const payments = await loadPayments({ signal });
signal.throwIfAborted();
return merge(users, payments);
}Não crie um novo timeout em cada camada sem coordenação. Propague um orçamento de tempo ou o mesmo signal, adicionando limites locais quando necessário.
Timers canceláveis
import { setTimeout } from 'node:timers/promises';
const controller = new AbortController();
const task = setTimeout(30_000, 'concluído', {
signal: controller.signal,
});
controller.abort();
try {
await task;
} catch (error) {
console.log(error.name);
}Timers tradicionais com callback continuam usando clearTimeout. A versão Promise integra melhor com AbortSignal.
Streams e pipeline
import { pipeline } from 'node:stream/promises';
await pipeline(source, transform, destination, {
signal,
});Ao abortar, pipeline destrói as streams compatíveis. Implemente limpeza em streams personalizadas e remova arquivos parciais.
Processos filhos
import { spawn } from 'node:child_process';
const child = spawn('programa', ['--modo', 'batch'], {
signal,
});O signal envia o cancelamento ao processo, mas processos descendentes e diferenças de plataforma exigem testes. Aplique prazo de encerramento.
fs e operações de arquivo
Algumas APIs de arquivo aceitam signal. O cancelamento pode impedir etapas adicionais, mas não garante interromper imediatamente toda chamada de sistema já em andamento. Consulte o contrato de cada função.
Função própria cancelável
async function processItems(items, { signal }) {
for (const item of items) {
signal.throwIfAborted();
await processItem(item);
}
}Esse é cancelamento cooperativo: a função verifica o signal em pontos seguros. Em loops CPU-bound, verifique periodicamente, mas não a cada instrução.
Promise com evento de abort
function waitForMessage(emitter, { signal }) {
return new Promise((resolve, reject) => {
function cleanup() {
emitter.off('message', onMessage);
signal.removeEventListener('abort', onAbort);
}
function onMessage(message) {
cleanup();
resolve(message);
}
function onAbort() {
cleanup();
reject(signal.reason);
}
if (signal.aborted) {
reject(signal.reason);
return;
}
emitter.once('message', onMessage);
signal.addEventListener('abort', onAbort, { once: true });
});
}Remova listeners ao concluir para evitar vazamentos e resolução duplicada.
Reason
Passe uma razão útil:
controller.abort({
code: 'CLIENT_DISCONNECTED',
requestId,
});Prefira Error ou um objeto pequeno. Não inclua dados pessoais. Bibliotecas podem esperar uma exceção padrão, então mantenha interoperabilidade.
Erro de timeout
Não transforme todo abort em HTTP 500. Exemplos de classificação:
- cliente desconectou: não há resposta a enviar;
- timeout interno: 504 ou erro de dependência;
- shutdown: interromper e permitir retry;
- cancelamento manual: resultado específico da operação.
Em logs, diferencie cancelamentos esperados de falhas inesperadas.
Deadline
Em sistemas distribuídos, um deadline absoluto pode ser mais útil que vários timeouts independentes:
const remainingMs = Math.max(0, deadline - Date.now());
const signal = AbortSignal.timeout(remainingMs);Valide deadlines recebidos e reserve margem para serialização e resposta.
Retries
Cada tentativa deve respeitar o signal global:
async function retry(operation, { signal, attempts = 3 }) {
let lastError;
for (let attempt = 1; attempt <= attempts; attempt += 1) {
signal.throwIfAborted();
try {
return await operation({ signal, attempt });
} catch (error) {
lastError = error;
if (signal.aborted || attempt === attempts) throw error;
await setTimeout(100 * attempt, undefined, { signal });
}
}
throw lastError;
}Não faça retry de operações não idempotentes sem chave de idempotência.
Promise.all
Quando uma operação falha, Promise.all não cancela automaticamente as restantes. Use um controller compartilhado:
const controller = new AbortController();
try {
return await Promise.all([
loadA({ signal: controller.signal }),
loadB({ signal: controller.signal }),
]);
} catch (error) {
controller.abort(error);
throw error;
}Worker Threads
AbortSignal não é um botão universal para interromper cálculo em outra thread. Envie mensagem de cancelamento ou use SharedArrayBuffer com flag. O worker precisa cooperar.
Shutdown
Crie um controller global para tarefas de longa duração:
const shutdownController = new AbortController();
process.once('SIGTERM', () => {
shutdownController.abort(new Error('Servidor encerrando'));
});Não misture signal global e signal por requisição sem combinar corretamente.
Observabilidade
Meça quantidade de cancelamentos por motivo, duração até cancelamento, timeouts por dependência e trabalho interrompido. Evite alertar como erro todo cancelamento de cliente.
Testes
Teste signal já abortado, abort durante I/O, operação concluída antes do abort, múltiplos listeners, timeout e limpeza de recursos. Use timers curtos controlados, não testes frágeis dependentes de rede externa.
Erros comuns
- criar timeout sem propagar signal;
- reutilizar signal abortado;
- não limpar listeners;
- tratar abort como erro 500;
- achar que Promise.all cancela tarefas;
- cancelar a Promise sem cancelar o recurso;
- ignorar arquivos e respostas parciais;
- usar retries depois do deadline;
- não testar desconexão do cliente.
Fluxo recomendado
Defina um orçamento, crie ou combine signals na borda, propague por todas as camadas, implemente limpeza e classifique razões. Combine com Streams e Backpressure, Web Streams API, child_process e Worker Threads.
Consulte a documentação oficial de AbortController e a referência oficial de AbortSignal.




