Os aplicativos web modernos frequentemente dependem de APIs para recuperar dados, autenticar usuários e processar informações. No entanto, as solicitações de rede nem sempre são concluídas conforme o esperado. Um servidor pode responder lentamente, uma conexão pode ser interrompida ou um usuário pode sair antes que uma solicitação seja concluída.
Essas situações podem criar experiências frustrantes se um aplicativo não tiver uma estratégia para lidar com elas.
JavaScript fornece vários mecanismos para gerenciar operações assíncronas, incluindo AbortControllerpromessas e sync/await. Ao combinar esses recursos cuidadosamente, os desenvolvedores podem tornar as solicitações de API mais fáceis de gerenciar e os aplicativos mais resilientes a falhas temporárias.
Este tutorial explica como funcionam os tempos limite de solicitação, o cancelamento e as estratégias de nova tentativa, com exemplos práticos que podem ser adaptados a aplicações baseadas em navegador.
Compreendendo o problema com solicitações básicas de busca
A API Fetch fornece uma maneira direta de recuperar informações de um servidor.
async function loadProducts() {
const response = await fetch("/api/products");
const products = await response.json();
return products;
}
Embora este exemplo seja simples, deixa várias questões importantes sem solução.
Primeiro, não verifica response.ok. A API Fetch geralmente resolve sua promessa mesmo quando o servidor retorna um erro HTTP como 404 Not Found ou 500 Internal Server Error.
Segundo, a solicitação não tem tempo limite explícito. Se a aplicação precisar parar de esperar após um determinado período, ela deverá implementar esse comportamento.
Finalmente, não há tratamento de erros voltado para o usuário. Uma solicitação de rede com falha ou uma resposta JSON inválida pode fazer com que a função seja rejeitada sem fornecer um caminho de recuperação útil.
Uma implementação mais confiável deveria levar em conta essas possibilidades.
1. Implementar tempos limite de solicitação com AbortController
Um tempo limite de solicitação permite que um aplicativo pare de esperar por uma resposta após um período definido.
JavaScript AbortController pode cancelar uma solicitação Fetch. Uma opção conveniente é AbortSignal.timeout()que cria um sinal que é abortado automaticamente após um número especificado de milissegundos.
async function fetchWithTimeout(url, timeoutMs = 5000) {
const signal = AbortSignal.timeout(timeoutMs);
const response = await fetch(url, { signal });
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return response.json();
}
A função acima define um tempo limite de cinco segundos por padrão. Se a solicitação exceder a duração especificada, a operação de busca será abortada.
Por exemplo:
try {
const products = await fetchWithTimeout(
"/api/products",
5000
);
console.log(products);
} catch (error) {
if (error.name === "TimeoutError") {
console.error("The request timed out.");
} else {
console.error("Unable to load products:", error);
}
}
Este exemplo distingue um tempo limite de outras falhas. Uma interface de produção também deve comunicar o problema em uma linguagem que os usuários possam entender.
Nota de compatibilidade: Verifique o suporte do navegador para AbortSignal.timeout() se o seu aplicativo for direcionado a navegadores mais antigos. Um gerenciamento manual AbortController e o temporizador pode ser usado quando necessário.
2. Adicionar lógica de nova tentativa para falhas temporárias
Algumas falhas de rede são temporárias. Por exemplo, um serviço pode ficar brevemente indisponível durante a implantação ou enfrentar um problema de conexão de curta duração.
Tentar novamente uma solicitação com falha pode melhorar a resiliência, mas as novas tentativas não devem ser automáticas para cada erro. Uma falha de autenticação ou uma solicitação inválida geralmente requer uma solução diferente.
O seguinte auxiliar tenta novamente uma operação com espera exponencial:
const sleep = (ms) =>
new Promise((resolve) => setTimeout(resolve, ms));
async function retry(operation, maxAttempts = 3) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await operation();
} catch (error) {
if (attempt === maxAttempts) {
throw error;
}
const delayMs = 500 * 2 ** (attempt - 1);
await sleep(delayMs);
}
}
}
O atraso aumenta após cada tentativa malsucedida:
-
Primeira tentativa: aproximadamente 500 milissegundos.
-
Segunda tentativa: aproximadamente 1.000 milissegundos.
-
Novas tentativas: atrasos progressivamente maiores.
O helper pode ser usado com uma solicitação:
const products = await retry(
() => fetchWithTimeout("/api/products"),
3
);
Este exemplo tenta novamente qualquer erro gerado pela operação, incluindo erros que podem não ser temporários. Uma implementação de produção deve classificar as falhas e tentar novamente apenas nos casos apropriados.
Deve também considerar uma duração máxima total, fornecida pelo servidor Retry-After instruções e se a operação é segura para ser repetida.
Por exemplo, repetir uma solicitação de produto somente leitura geralmente é menos arriscado do que repetir uma solicitação de pagamento que já pode ter sido bem-sucedida no servidor.
3. Cancelar solicitações que não são mais necessárias
Os tempos limite são úteis quando as solicitações demoram muito, mas algumas solicitações se tornam desnecessárias por outros motivos.
Considere uma interface de pesquisa onde os usuários digitam em um campo de entrada. Se cada pressionamento de tecla acionar uma nova solicitação de API, diversas solicitações poderão permanecer em andamento simultaneamente.
Uma solicitação mais antiga pode terminar após uma mais recente e substituir os resultados da pesquisa mais recentes.
Um AbortController permite que o aplicativo cancele a solicitação anterior antes de iniciar outra.
let activeController;
async function searchProducts(query) {
if (activeController) {
activeController.abort();
}
activeController = new AbortController();
try {
const response = await fetch(
`/api/products?q=${encodeURIComponent(query)}`,
{ signal: activeController.signal }
);
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return await response.json();
} catch (error) {
if (error.name === "AbortError") {
return null;
}
throw error;
}
}
Cada nova pesquisa cancela a solicitação anterior. O encodeURIComponent() A função também garante que a consulta de pesquisa seja codificada com segurança como um parâmetro de URL.
Retornando null para um cancelamento intencional permite que o código de chamada distinga uma solicitação abortada de uma resposta bem-sucedida. A interface deve tratar esse resultado explicitamente.
Para aplicações com múltiplos campos de pesquisa independentes, o controlador deve ser mantido separadamente para cada campo, em vez de armazenado em uma variável compartilhada.
4. Lide com erros no nível certo
Uma função de API reutilizável deve relatar falhas ao seu chamador, em vez de fingir silenciosamente que uma operação foi bem-sucedida.
Por exemplo, uma função que recupera produtos pode gerar um erro quando o servidor retornar um status de falha. A interface pode então decidir se deseja mostrar um botão de nova tentativa, exibir um estado vazio ou apresentar uma mensagem.
async function displayProducts() {
const container = document.querySelector("#products");
container.textContent = "Loading products...";
try {
const products = await retry(
() => fetchWithTimeout("/api/products", 5000),
3
);
container.replaceChildren();
for (const product of products) {
const item = document.createElement("p");
item.textContent = product.name ?? "Unnamed product";
container.appendChild(item);
}
} catch (error) {
console.error("Product request failed:", error);
container.textContent =
"We couldn't load the products. Please try again.";
}
}
Este exemplo pressupõe que a página contém um elemento com o ID products e que a API retorna uma matriz de objetos de produto.
Usando textContent em vez de inserir nomes fornecidos pela API por meio innerHTML evita interpretar nomes de produtos como marcação HTML.
A separação entre recuperação de dados e comportamento da interface também torna o código mais fácil de testar e manter.
5. Cenários de falha de teste, não apenas solicitações bem-sucedidas
Uma implementação de tratamento de solicitações deve ser testada em mais do que uma resposta de API bem-sucedida.
Considere estes cenários:
| Caso de teste | Comportamento esperado |
|---|---|
| Servidor retorna HTTP 200 | Analisar e retornar os dados de resposta |
| Servidor retorna HTTP 500 | Relatar um erro HTTP |
| A conexão de rede falha | Lidar com a solicitação rejeitada |
| A solicitação excede o tempo limite | Abortar a solicitação |
| Ocorre uma falha temporária | Tente novamente se o erro for elegível |
| O usuário inicia uma nova pesquisa | Cancelar a solicitação anterior |
| Todas as tentativas falham | Mostrar uma mensagem de erro útil |
Esses testes ajudam a revelar problemas que, de outra forma, poderiam aparecer apenas na produção.
Para testes automatizados, os desenvolvedores podem simular respostas do Fetch, simular promessas rejeitadas e verificar se a lógica de novas tentativas para após o número configurado de tentativas.
Também é importante verificar se uma solicitação com falha não deixa um indicador de carregamento visível indefinidamente.
Conclusão
Comunicação API confiável requer mais do que ligar fetch() e analisando JSON. Os desenvolvedores devem decidir por quanto tempo uma solicitação deve ser executada, quando uma operação com falha deve ser repetida e o que deve acontecer quando uma solicitação não for mais relevante.
AbortController e AbortSignal.timeout() fornecem ferramentas úteis para cancelamento e intervalos. A espera exponencial pode ajudar a gerenciar falhas temporárias, enquanto o tratamento consistente de erros permite que a interface responda de maneira previsível.
A abordagem mais eficaz depende da aplicação. As solicitações somente leitura podem se beneficiar de novas tentativas cuidadosamente controladas, enquanto os pagamentos e outras operações de alteração de estado exigem proteções mais fortes contra processamento duplicado.
Ao combinar políticas explícitas de tempo limite, novas tentativas seletivas, cancelamento e tratamento significativo de erros, os desenvolvedores podem criar aplicativos que respondam de maneira mais previsível às realidades da comunicação em rede.




