Cancelando Requisições Fetch em Andamento com AbortController

Aplicações web modernas frequentemente fazem requisições HTTP que os usuários podem não precisar completar. Um usuário digitando em uma caixa de pesquisa gera múltiplas requisições, mas apenas a mais recente importa. Um usuário navegando para fora de uma página torna qualquer requisição pendente para aquela página irrelevante. Sem o cancelamento adequado, essas requisições desnecessárias desperdiçam largura de banda, consomem recursos do servidor e podem causar a exibição de dados desatualizados na sua interface.
A API AbortController fornece uma forma limpa e padronizada de cancelar requisições fetch e outras operações assíncronas. Este artigo mostra como implementar o cancelamento de requisições usando AbortController, cobrindo padrões práticos como debouncing de pesquisa, limpeza de componentes e tratamento de timeout.
Principais Pontos
- AbortController cria um sinal que o fetch monitora para eventos de cancelamento
- Sempre verifique por AbortError em blocos catch para distinguir cancelamentos de falhas
- Cancele requisições anteriores em interfaces de pesquisa para prevenir condições de corrida
- Use funções de limpeza em componentes de frameworks para cancelar requisições na desmontagem
- Combine AbortController com setTimeout para funcionalidade de timeout de requisições
- Cada AbortController é de uso único - crie novos para lógica de retry
- Node.js 18+ inclui suporte nativo ao AbortController
O que AbortController e AbortSignal Fazem
AbortController cria um objeto controlador que gerencia cancelamento através de um AbortSignal associado. O controlador tem um trabalho: chamar abort()
quando você quer cancelar uma operação. O sinal atua como um canal de comunicação que fetch e outras APIs monitoram para eventos de cancelamento.
const controller = new AbortController()
const signal = controller.signal
// O sinal começa como não abortado
console.log(signal.aborted) // false
// Chamar abort() muda o estado do sinal
controller.abort()
console.log(signal.aborted) // true
Quando você passa um sinal para fetch e depois chama abort()
, a promise do fetch rejeita com uma DOMException
chamada AbortError
. Isso permite distinguir entre cancelamentos e falhas reais de rede.
Implementação Básica do AbortController
Para cancelar uma requisição fetch, crie um AbortController, passe seu sinal para fetch, então chame abort()
quando necessário:
const controller = new AbortController()
fetch('/api/data', { signal: controller.signal })
.then(response => response.json())
.then(data => console.log('Dados recebidos:', data))
.catch(error => {
if (error.name === 'AbortError') {
console.log('Requisição foi cancelada')
} else {
console.error('Requisição falhou:', error)
}
})
// Cancelar a requisição
controller.abort()
Os pontos principais:
- Crie o controlador antes de fazer a requisição
- Passe
controller.signal
para as opções do fetch - Chame
controller.abort()
para cancelar - Verifique por
AbortError
no seu bloco catch
Como Cancelar Requisições de Pesquisa Quando Usuários Digitam
Interfaces de pesquisa frequentemente disparam requisições a cada tecla pressionada. Sem cancelamento, respostas lentas podem chegar fora de ordem, mostrando resultados para consultas desatualizadas. Aqui está como cancelar requisições de pesquisa anteriores:
let searchController = null
function performSearch(query) {
// Cancelar qualquer pesquisa existente
if (searchController) {
searchController.abort()
}
// Criar novo controlador para esta pesquisa
searchController = new AbortController()
fetch(`/api/search?q=${encodeURIComponent(query)}`, {
signal: searchController.signal
})
.then(response => response.json())
.then(results => {
console.log('Resultados da pesquisa:', results)
updateSearchUI(results)
})
.catch(error => {
if (error.name === 'AbortError') {
console.log('Pesquisa cancelada')
} else {
console.error('Pesquisa falhou:', error)
showSearchError()
}
})
}
// Uso: cada nova pesquisa cancela a anterior
performSearch('javascript')
performSearch('javascript frameworks') // Cancela a pesquisa por 'javascript'
Este padrão garante que apenas os resultados de pesquisa mais recentes apareçam na sua interface, prevenindo condições de corrida onde requisições mais lentas sobrescrevem as mais rápidas.
Cancelando Requisições na Desmontagem de Componentes
Em frameworks frontend, componentes frequentemente fazem requisições fetch que devem ser canceladas quando o componente é desmontado. Isso previne vazamentos de memória e erros de tentar atualizar componentes desmontados.
Exemplo React
import { useEffect, useState } from 'react'
function UserProfile({ userId }) {
const [user, setUser] = useState(null)
const [loading, setLoading] = useState(true)
useEffect(() => {
const controller = new AbortController()
async function fetchUser() {
try {
const response = await fetch(`/api/users/${userId}`, {
signal: controller.signal
})
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
const userData = await response.json()
setUser(userData)
} catch (error) {
if (error.name !== 'AbortError') {
console.error('Falha ao buscar usuário:', error)
}
} finally {
if (!controller.signal.aborted) {
setLoading(false)
}
}
}
fetchUser()
// Função de limpeza cancela a requisição
return () => controller.abort()
}, [userId])
if (loading) return <div>Carregando...</div>
return <div>{user?.name}</div>
}
Exemplo Vue
export default {
data() {
return {
user: null,
controller: null
}
},
async mounted() {
this.controller = new AbortController()
try {
const response = await fetch(`/api/users/${this.userId}`, {
signal: this.controller.signal
})
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
this.user = await response.json()
} catch (error) {
if (error.name !== 'AbortError') {
console.error('Falha ao buscar usuário:', error)
}
}
},
beforeUnmount() {
if (this.controller) {
this.controller.abort()
}
}
}
Implementando Timeouts de Requisição com AbortController
Requisições de rede podem travar indefinidamente. AbortController combinado com setTimeout
fornece um mecanismo limpo de timeout:
function fetchWithTimeout(url, options = {}, timeoutMs = 5000) {
const controller = new AbortController()
const timeoutId = setTimeout(() => {
controller.abort()
}, timeoutMs)
return fetch(url, {
...options,
signal: controller.signal
}).finally(() => {
clearTimeout(timeoutId)
})
}
// Uso
fetchWithTimeout('/api/slow-endpoint', {}, 3000)
.then(response => response.json())
.then(data => console.log('Dados:', data))
.catch(error => {
if (error.name === 'AbortError') {
console.log('Requisição expirou')
} else {
console.error('Requisição falhou:', error)
}
})
Para lógica de timeout reutilizável, crie uma função utilitária:
function createTimeoutSignal(timeoutMs) {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), timeoutMs)
// Limpar timeout quando o sinal for usado
controller.signal.addEventListener('abort', () => {
clearTimeout(timeoutId)
}, { once: true })
return controller.signal
}
// Uso
fetch('/api/data', { signal: createTimeoutSignal(5000) })
.then(response => response.json())
.catch(error => {
if (error.name === 'AbortError') {
console.log('Requisição expirou ou foi cancelada')
}
})
Tratamento Adequado de Erros para Requisições Canceladas
Sempre distinga entre cancelamentos e erros genuínos. AbortError indica cancelamento intencional, não uma falha:
async function handleRequest(url) {
const controller = new AbortController()
try {
const response = await fetch(url, { signal: controller.signal })
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
return await response.json()
} catch (error) {
if (error.name === 'AbortError') {
// Isso é esperado quando cancelamos - não registrar como erro
console.log('Requisição foi cancelada')
return null
}
// Este é um erro real que precisa ser tratado
console.error('Requisição falhou:', error)
throw error
}
}
Para aplicações com rastreamento de erros, exclua AbortError dos relatórios de erro:
.catch(error => {
if (error.name === 'AbortError') {
// Não reportar cancelamentos para rastreamento de erros
return
}
// Reportar erros reais
errorTracker.captureException(error)
throw error
})
Gerenciando Múltiplas Requisições
Ao lidar com múltiplas requisições concorrentes, você pode precisar cancelar todas de uma vez ou gerenciá-las individualmente:
class RequestManager {
constructor() {
this.controllers = new Map()
}
async fetch(key, url, options = {}) {
// Cancelar requisição existente com a mesma chave
this.cancel(key)
const controller = new AbortController()
this.controllers.set(key, controller)
try {
const response = await fetch(url, {
...options,
signal: controller.signal
})
return response
} finally {
this.controllers.delete(key)
}
}
cancel(key) {
const controller = this.controllers.get(key)
if (controller) {
controller.abort()
this.controllers.delete(key)
}
}
cancelAll() {
for (const controller of this.controllers.values()) {
controller.abort()
}
this.controllers.clear()
}
}
// Uso
const requestManager = new RequestManager()
// Essas requisições podem ser gerenciadas independentemente
requestManager.fetch('user-profile', '/api/user/123')
requestManager.fetch('user-posts', '/api/user/123/posts')
// Cancelar requisição específica
requestManager.cancel('user-profile')
// Cancelar todas as requisições pendentes
requestManager.cancelAll()
Suporte de Navegadores e Compatibilidade Node.js
AbortController tem excelente suporte de navegadores:
- Chrome 66+
- Firefox 57+
- Safari 12.1+
- Edge 16+
Para Node.js, AbortController está disponível nativamente no Node 18+. Para versões anteriores, use o polyfill abort-controller:
npm install abort-controller
// Para Node.js < 18
const { AbortController } = require('abort-controller')
// Usar normalmente
const controller = new AbortController()
Conclusão
AbortController fornece uma forma limpa e padronizada de cancelar requisições fetch em aplicações web modernas. Os padrões principais são: criar um controlador antes de cada requisição, passar seu sinal para fetch, e chamar abort() quando o cancelamento for necessário. Sempre trate AbortError separadamente de falhas genuínas de rede para evitar tratar cancelamentos intencionais como erros.
Os casos de uso mais comuns—gerenciamento de requisições de pesquisa, limpeza de componentes e tratamento de timeout—seguem padrões diretos que você pode adaptar às suas necessidades específicas. Com implementação adequada, o cancelamento de requisições melhora tanto a experiência do usuário quanto a performance da aplicação, prevenindo atividade desnecessária de rede e atualizações de dados desatualizados.
Pronto para implementar cancelamento de requisições na sua aplicação? Comece com o padrão básico para seu caso de uso mais comum, então expanda para cenários mais complexos conforme necessário. O investimento em gerenciamento adequado de requisições paga dividendos na performance da aplicação e experiência do usuário.
Perguntas Frequentes
Não, uma vez que abort() é chamado, o sinal do controlador permanece no estado abortado permanentemente. Você deve criar um novo AbortController para cada nova requisição ou tentativa de retry.
Não, abort() apenas previne que seu código JavaScript processe a resposta. Se a requisição já foi enviada pela rede, o servidor ainda pode recebê-la e processá-la.
Crie múltiplos controladores e armazene-os em um array ou Map, então chame abort() em cada um. Alternativamente, use um único controlador e passe seu sinal para múltiplas chamadas fetch - chamar abort() uma vez cancelará todas elas.
Nada acontece. A chamada abort() é ignorada se a requisição já terminou com sucesso ou falhou por outras razões.
Sim, AbortController funciona identicamente tanto com chains de Promise quanto com async/await. A promise do fetch rejeitará com um AbortError quando cancelada, que você pode capturar com blocos try/catch.