Como aplicar um timeout ou cancelar uma requisição com o fetch do JavaScript
Aplicar um timeout em uma requisição é útil para evitar que uma aplicação fique presa esperando por uma resposta que pode não chegar, seja por problemas na rede ou no próprio servidor.
Além disso, outro recurso importante para ampliar o controle sobre as requisições de uma aplicação é a capacidade de cancelamento, que pode ser iniciada pelo usuário ou por regras de negócio. Ao cancelar uma requisição, interrompemos a transmissão de dados, reduzindo o tráfego e aliviando a carga de processamento.
Neste texto, vamos explorar como aplicar timeouts em requisições utilizando a função fetch
do JavaScript e como cancelar uma requisição ainda em processamento.
Timeout com Promise.race
Antes da introdução de mecanismos de cancelamento nativos no fetch, era comum usar o Promise.race
para criar o mecanismo de timeout nas requisições. A ideia era a seguinte: o fetch retorna uma promise, que representa uma promessa de valor futuro. Em paralelo, criamos outra promise que lança um erro após um determinado período de tempo. Ao usar o Promise.race
, colocamos as duas promises para "competir". A primeira promise a ser resolvida, seja com sucesso ou com erro, é a que será utilizada.
function request(url, timeoutEmMS = 3000) {
return Promise.race([
fetch(url),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), timeoutEmMS)
)
]);
}
É importante notar que o Promise.race
não cancela a requisição fetch em si, apenas ignora a resposta caso o tempo limite seja excedido. A seguir vamos abordar os métodos mais modernos de cancelamento, como o AbortController
.
Definindo o timeout de uma requisição com o fetch e AbortSignal
Configurar o timeout utilizando o AbortSignal
é extremamente simples, basta utilizar o AbortSignal.timeout(valor em milissegundos)
:
async function request(url, timeoutEmMS = 3000) {
return fetch(url, {
signal: AbortSignal.timeout(timeoutEmMS),
});
}
TheAbortSignal
interface represents a signal object that allows you to communicate with an asynchronous operation (such as a fetch request) and abort it if required via anAbortController
object.
Fonte: link
Como cancelar uma requisição em andamento utilizando fetch e AbortController
Um outro mecanismo para cancelar uma requisição em andamento é através do AbortController
:
async function request(url, timeoutEmMS = 3000) {
const controller = new AbortController();
// Aqui normalmente adicionamos algum
// eventListener para chamar o método
// controller.abort(). Assim a requisição
// é cancelada
return fetch(url, {
signal: controller.signal
});
}
The AbortController
interface represents a controller object that allows you to abort one or more Web requests as and when desired.
You can create a newAbortController
object using theAbortController()
constructor. Communicating with an asynchronous operation is done using anAbortSignal
object.
Fonte: link
Testando o timeout e cancelamento juntos
Para testar estes dois mecanismos, o timeout e o cancelamento, criei um pequeno cenário de estudo. O código abaixo, executado com Node.js na versão 20.12.0, tem dois endpoints:
- localhost:3000/home
retorna o conteúdo do arquivo index.html (logo abaixo) - localhost:3000/
retorna um JSON com um delay de 4 segundos. Este atraso é proposital para facilitar os testes
Para executar a aplicação utilizei o seguinte comando:
node --experimental-default-type module server.js
Após executar o projeto, basta abrir o endereço localhost:3000/home no navegador e executar os testes, conforme a imagem abaixo ilustra:
Tanto o timeout quanto o cancelamento voluntário funcionaram perfeitamente, interrompendo a requisição em execução. Também é interessante notar que, a mensagem de erro varia conforme o navegador, mas mantém o mesmo significado. Abaixo está o resultado da exceção para Timeout ou cancelamento da requisição em 3 navegadores, detalhando os campos name, code e message do objeto de erro.
Safari (17.4.1)
- Timeout:
- error.name: AbortError
- error.code: 20
- error.message: Fetch is aborted
- Abort:
- error.name: AbortError
- error.code: 20
- error.message: Fetch is aborted
Chrome (124.0.6367.79 (Official Build) (arm64))
- Timeout:
- error.name: TimeoutError
- error.code: 23
- error.message: signal timed out
- Abort:
- error.name: AbortError
- error.code: 20
- error.message: signal is aborted without reason
Firefox Developer Edition (125.0b3 (64-bit))
- Timeout:
- error.name: TimeoutError
- error.code: 23
- error.message: The operation timed out
- Abort:
- error.name: AbortError
- error.code: 20
- error.message: The operation was aborted
Considerações finais
Aplicar timeout em requisições HTTP é uma prática recomendada. Ao definir limites de tempo para esperar por respostas do servidor, podemos evitar que nossa aplicação fique presa aguardando indefinidamente e proporcionar uma experiência mais fluida para os usuários.
Falando rapidamente do suporte, a maioria dos navegadores modernos já dá suporte ao AbortSignal/AbortController
. Para mais detalhes, consulte este link. Já para Node.js, o AbortController
está disponível de forma definitiva a partir da versão 15.4.0 conforme este link.