
Canceling Stale Network Requests in JavaScript with AbortController
Asynchronous race conditions are a common bug in interactive web applications. When a user types quickly into a search box or switches between dashboard tabs, multiple fetch() calls are fired in sequence. If an earlier request takes longer over the network than a later request, the stale response resolves last and overwrites the UI with outdated results.
By pairing fetch() with the browser's native AbortController interface, we can pass an AbortSignal to every outgoing request and immediately cancel any in-flight call before starting a new one.
Example
const controller = new AbortController();
// Pass controller.signal in the fetch options object
fetch("/api/search?q=clean+markup", { signal: controller.signal })
.then((response) => response.json())
.then((data) => console.log(data))
.catch((error) => {
if (error.name === "AbortError") {
console.log("Previous request canceled cleanly.");
}
});
// Cancel the request if a newer query starts
controller.abort();
Input : Rapidly changing search query or tab selection
Output : Only the latest network request resolves while earlier in-flight requests are aborted immediately.
Implementation
function createSearchClient(endpoint) {
let activeController = null;
return async function searchQueries(query) {
// Cancel the previous pending request if one is still running
if (activeController) {
activeController.abort();
}
activeController = new AbortController();
const { signal } = activeController;
try {
const url = `${endpoint}?q=${encodeURIComponent(query)}`;
const response = await fetch(url, {
method: "GET",
headers: { Accept: "application/json" },
signal,
});
if (!response.ok) {
throw new Error(`HTTP error ${response.status}`);
}
const payload = await response.json();
activeController = null;
return payload.results;
} catch (error) {
if (error.name === "AbortError") {
// Ignore expected cancellations triggered by newer keystrokes
return null;
}
throw error;
}
};
}
const fetchSuggestions = createSearchClient("/api/articles");
const searchInput = document.querySelector("#search-input");
const resultsList = document.querySelector("#search-results");
searchInput.addEventListener("input", async (event) => {
const query = event.target.value.trim();
if (!query) {
resultsList.innerHTML = "";
return;
}
const results = await fetchSuggestions(query);
if (results === null) {
// Request was superseded by a newer input event
return;
}
resultsList.innerHTML = results
.map((item) => `<li><a href="${item.url}">${item.title}</a></li>`)
.join("");
});
Main Point: Always check for error.name === "AbortError" in your catch block so intentional request cancellations are handled silently instead of triggering user-facing error banners.
Credits
Photo by Luke Chesser