// apis · Web Platform Advent #20
AbortController: cancelar fetches, tempos limite e listeners de eventos
Um único controlador cancela um fetch, um tempo limite e uma pilha de listeners de eventos. Como funcionam o AbortController e o AbortSignal, por que um cancelamento manual e um tempo limite lançam erros diferentes, e o truque de limpeza que quase todo mundo deixa passar.
Iniciar uma operação assíncrona é fácil. Parar uma com a qual você já não se importa é a parte que a maioria do código pula, e é daí que vêm os vazamentos de memória, as condições de corrida e os resultados desatualizados. O AbortController é a resposta única do navegador para tudo isso.
Um controlador, um signal
A MDN define isso sem rodeios: a interface AbortController representa um objeto controlador que permite abortar uma ou mais requisições Web quando você quiser. O formato é sempre o mesmo. Você cria um controlador, lê o seu signal e entrega esse signal ao que está iniciando.
A propriedade signal retorna uma instância do objeto AbortSignal, que pode ser usada para se comunicar com uma operação assíncrona, ou para abortá-la. O controlador é o que você guarda; o signal é o que você entrega.
const controller = new AbortController();
const res = await fetch('/api/articles', { signal: controller.signal });
// em outro lugar, mais tarde:
controller.abort(); Repare na direção. O código que faz o trabalho nunca decide parar; ele apenas concorda em ouvir. É essa separação que permite a um único chamador cancelar muitas operações de uma só vez.
Os dois erros, e por que isso importa
Esta é a parte que vale ler com atenção, porque o mesmo cancelamento pode lançar duas coisas diferentes.
Quando você mesmo chama abort(), a promise de fetch() é rejeitada com uma DOMException chamada AbortError. Quando o signal veio de AbortSignal.timeout(), a promise de fetch() é rejeitada com uma DOMException TimeoutError em vez disso.
Então a verificação de sempre está incompleta:
try {
const res = await fetch(url, { signal });
} catch (err) {
if (err.name === 'AbortError') {
// o usuário ou o nosso próprio código cancelou
} else if (err.name === 'TimeoutError') {
// o prazo expirou - uma situação completamente diferente
} else {
throw err; // uma falha real de rede ou de parsing
}
} Juntar os dois casos faz perder informação real. Um usuário que sai da página não é um problema; uma requisição que estourou o tempo provavelmente é, e vale a pena registrá-la ou tentar de novo. O código que só procura AbortError trata cada tempo limite como um não-evento silencioso.
Tempos limite sem encanamento
O padrão que a maioria ainda escreve combina um controlador com um setTimeout e um clearTimeout num bloco finally. Funciona, e já não é necessário.
AbortSignal.timeout() retorna uma instância de AbortSignal que abortará automaticamente após um tempo especificado. Sem controlador, sem temporizador para limpar, nada que possa vazar:
// Antes: três peças móveis
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 5000);
try {
const res = await fetch(url, { signal: controller.signal });
} finally {
clearTimeout(timer);
}
// Agora: uma
const res = await fetch(url, { signal: AbortSignal.timeout(5000) }); Fique de olho no nome do erro ao fazer essa troca. A versão antiga lançava AbortError; a nova lança TimeoutError, então qualquer bloco catch que você já tivesse vai precisar ser atualizado.
O truque que quase todo mundo deixa passar: os listeners de eventos
O AbortSignal não é um recurso do fetch. addEventListener aceita um signal entre suas opções e, quando esse signal aborta, o listener é removido para você. Isso substitui discretamente o código de limpeza mais tedioso do trabalho de front-end.
Compare com remover listeners na mão, o que exige guardar uma referência para cada função que você passou:
// Antes: você precisa guardar cada handler para removê-lo
function onScroll() { /* ... */ }
function onResize() { /* ... */ }
window.addEventListener('scroll', onScroll);
window.addEventListener('resize', onResize);
function teardown() {
window.removeEventListener('scroll', onScroll);
window.removeEventListener('resize', onResize);
} Com um signal, os handlers podem voltar a ser inline, e uma única chamada remove todos eles:
const controller = new AbortController();
const { signal } = controller;
window.addEventListener('scroll', () => { /* ... */ }, { signal });
window.addEventListener('resize', () => { /* ... */ }, { signal });
document.addEventListener('keydown', () => { /* ... */ }, { signal });
// uma chamada remove os três
controller.abort(); O mesmo controlador também pode estar segurando o signal de um fetch em andamento. Desmontar uma view vira então um único abort() que cancela a requisição e desliga todos os listeners junto.
Combinar signals e ler o estado
AbortSignal.any() retorna um AbortSignal que aborta quando qualquer um dos signals de aborto fornecidos aborta. É assim que você consegue um prazo e um cancelamento manual na mesma operação:
const controller = new AbortController();
const signal = AbortSignal.any([
controller.signal, // o usuário apertou Cancelar
AbortSignal.timeout(10000), // ou passaram dez segundos
]);
const res = await fetch(url, { signal }); Um signal também diz em que pé está. aborted é um booleano que indica se a requisição ou requisições com as quais o signal está se comunicando estão abortadas, e reason é um valor JavaScript que fornece o motivo do aborto, depois que o signal abortou. Passar o seu próprio motivo para abort() é como você distingue as causas mais tarde:
controller.abort(new Error('User navigated away'));
// mais tarde
if (signal.aborted) console.log(signal.reason.message); Há também um evento abort, invocado quando as operações assíncronas com as quais o signal está se comunicando são abortadas, o que permite que o seu próprio código de longa duração coopere, e não apenas as APIs nativas.
Tornar a sua própria função abortável
Tudo o que aceita um signal entra no mesmo sistema. O padrão é verificar aborted antes de começar e ouvir o evento abort durante a execução:
function wait(ms, { signal } = {}) {
return new Promise((resolve, reject) => {
if (signal?.aborted) return reject(signal.reason);
const id = setTimeout(resolve, ms);
signal?.addEventListener('abort', () => {
clearTimeout(id);
reject(signal.reason);
}, { once: true });
});
} Agora o seu próprio utilitário pode ser cancelado pelo mesmo controlador que cancela os seus fetches, que é justamente o motivo de existir um mecanismo só em vez de vários.
Referência rápida
| Objetivo | Chamada |
|---|---|
| Cancelar manualmente | controller.abort(), lança AbortError |
| Adicionar um prazo | AbortSignal.timeout(ms), lança TimeoutError |
| Qualquer um dos dois | AbortSignal.any([a, b]) |
| Remover um listener automaticamente | addEventListener(type, fn, { signal }) |
| Verificar o estado | signal.aborted |
| Descobrir o motivo | signal.reason |
| Reagir ao cancelamento | o evento abort |
O AbortController é uma API pequena que resolve discretamente uma grande categoria de problemas de limpeza. Entregue um signal às suas requisições, aos seus listeners e aos seus próprios utilitários assíncronos, e depois pare todos eles com uma única chamada. Só lembre qual erro você está capturando, porque um aborto manual e um prazo expirado não são o mesmo evento.
Para o lado das requisições, veja o nosso guia da Fetch API em JavaScript.
Perguntas frequentes
- Para que serve o AbortController?
- A MDN o descreve como um objeto controlador que permite abortar uma ou mais requisições Web quando você quiser. Você cria um controlador, entrega o signal dele ao que iniciou e chama abort() quando quer parar. Não se limita ao fetch: qualquer coisa que aceite um AbortSignal pode ser cancelada da mesma forma.
- Que erro um fetch abortado lança?
- Depende de como ele foi abortado, e é aí que muita gente tropeça. Quando você mesmo chama AbortController.abort(), a promise do fetch é rejeitada com uma DOMException chamada AbortError. Quando o signal veio de AbortSignal.timeout(), a promise é rejeitada com uma DOMException TimeoutError em vez disso. O código que só verifica AbortError vai ignorar silenciosamente o caso do tempo limite.
- Como adiciono um tempo limite a um fetch?
- AbortSignal.timeout() retorna uma instância de AbortSignal que abortará automaticamente após um tempo especificado, então você passa AbortSignal.timeout(5000) como signal e não precisa de controlador, nem de setTimeout, nem de clearTimeout. Lembre-se de que ele rejeita com TimeoutError, e não com AbortError.
- Um controlador pode cancelar várias coisas de uma vez?
- Sim, e é justamente esse o sentido do nome. Um signal pode ser entregue a quantas operações você quiser, de modo que uma única chamada a abort() para todos os fetches e remove todos os listeners de eventos que o receberam. É a limpeza mais organizada disponível quando uma view é desmontada.
- Posso combinar um tempo limite com um cancelamento manual?
- AbortSignal.any() retorna um AbortSignal que aborta quando qualquer um dos signals de aborto fornecidos aborta. Passe a ele o signal do seu controlador e um signal de tempo limite, e a operação para no que acontecer primeiro.