AbortController: Dominando o Cancelamento de Operações Assíncronas
O AbortController é uma API moderna do JavaScript que permite cancelar operações assíncronas como requisições fetch, timers e streams. Antes dessa API, não havia um padrão consistente para interromper operações em andamento, levando a vazamento de memória e comportamentos inesperados. Entender como implementá-lo corretamente é essencial para construir aplicações robustas e eficientes.
Por que precisamos cancelar operações?
Considere um cenário real: um usuário digita em um campo de busca, mas muda de ideia antes de clicar em buscar. Se a requisição foi feita e ainda está em trânsito, você quer cancelá-la para evitar processamento desnecessário, atualizar a UI com dados obsoletos ou desperdiçar banda de rede. O AbortController resolve exatamente esse problema.
Conceitos Fundamentais
O que é AbortController?
AbortController é um objeto que fornece dois componentes: o controlador (AbortController) e o sinal (AbortSignal). O controlador permite enviar um comando de cancelamento, enquanto o sinal é passado para a operação assíncrona que será monitorada.
const controller = new AbortController();
const signal = controller.signal;
// signal é passado para operações que o suportam
// controller.abort() cancela tudo que está escutando esse signal
Anatomia básica
const controller = new AbortController();
// Monitorar se a operação foi abortada
controller.signal.addEventListener('abort', () => {
console.log('Operação cancelada!');
});
// Cancelar após 3 segundos
setTimeout(() => controller.abort(), 3000);
// Verificar se já foi abortado
console.log(controller.signal.aborted); // true/false
Usando AbortController com Fetch
Cancelamento manual de requisições
O caso mais comum é cancelar requisições HTTP. Veja como implementar corretamente:
const controller = new AbortController();
// Começar requisição
fetch('https://api.example.com/dados', {
signal: controller.signal
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => {
if (error.name === 'AbortError') {
console.log('Requisição foi cancelada');
} else {
console.error('Erro na requisição:', error);
}
});
// Cancelar após 5 segundos
setTimeout(() => controller.abort(), 5000);
Timeout automático com AbortSignal.timeout()
A forma mais elegante é usar AbortSignal.timeout(), que cancela automaticamente após um tempo específico:
// Cancela automaticamente após 5 segundos
fetch('https://api.example.com/dados', {
signal: AbortSignal.timeout(5000)
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => {
if (error.name === 'AbortError') {
console.log('Requisição expirou');
}
});
Padrões Avançados
Gerenciar múltiplas operações com um único sinal
Você pode reutilizar o mesmo sinal para cancelar várias operações em conjunto:
const controller = new AbortController();
Promise.all([
fetch('/api/usuarios', { signal: controller.signal }),
fetch('/api/posts', { signal: controller.signal }),
fetch('/api/comentarios', { signal: controller.signal })
])
.then(responses => Promise.all(responses.map(r => r.json())))
.then(data => console.log('Todos os dados carregados:', data))
.catch(error => {
if (error.name === 'AbortError') {
console.log('Uma ou mais requisições foram canceladas');
}
});
// Um cancel afeta todas as requisições
setTimeout(() => controller.abort(), 3000);
Combinar sinais com AbortSignal.any()
Para cenários onde você quer que a primeira operação que terminar vença, use AbortSignal.any():
const userController = new AbortController();
const timeoutSignal = AbortSignal.timeout(10000);
// O que terminar primeiro (usuário cancela ou timeout de 10s)
const combinedSignal = AbortSignal.any([
userController.signal,
timeoutSignal
]);
fetch('/api/dados-pesados', { signal: combinedSignal })
.then(r => r.json())
.then(data => console.log(data))
.catch(error => {
if (error.name === 'AbortError') {
console.log('Operação cancelada ou expirou');
}
});
// Usuário pode cancelar manualmente
document.getElementById('cancelBtn').addEventListener('click', () => {
userController.abort();
});
Implementação em uma classe real
class SearchAPI {
constructor() {
this.controller = null;
}
async buscar(termo) {
// Cancelar busca anterior se houver
if (this.controller) {
this.controller.abort();
}
this.controller = new AbortController();
try {
const response = await fetch(
`https://api.example.com/search?q=${termo}`,
{ signal: this.controller.signal }
);
const data = await response.json();
return data;
} catch (error) {
if (error.name === 'AbortError') {
console.log('Busca cancelada');
} else {
throw error;
}
}
}
cancelar() {
if (this.controller) {
this.controller.abort();
}
}
}
// Uso
const search = new SearchAPI();
search.buscar('javascript');
setTimeout(() => search.cancelar(), 2000);
Conclusão
Três aprendizados principais:
-
AbortController resolve um problema real: evita requisições fantasma, vazamento de memória e comportamentos inesperados em aplicações modernas. Use-o sempre que trabalhar com operações assíncronas que podem ser interrompidas.
-
Use
AbortSignal.timeout()para timeouts simples: não precisa de lógica complexa comsetTimeout. A API oferece exatamente o que você precisa de forma limpa. -
Combine sinais para cenários complexos:
AbortSignal.any()permite orquestrar múltiplas operações com elegância, cobrindo casos onde timeout do usuário e timeout automático precisam coexistir.
A prática consistente com essas técnicas transformará suas aplicações em código mais profissional e confiável.