Modern web applications often feature live search or autocomplete fields: as the user types, results appear almost instantly.
To provide a smooth experience we need to solve two important problems:
- Don't send an HTTP request after every single keystroke.
- When the user types quickly → cancel outdated requests so the UI doesn't show stale or wrong results.
In this article we'll implement exactly this behavior using only vanilla JavaScript — no libraries, no frameworks, no build tools.
Goals of the implementation
- Listen and Debounce input (wait a short time after typing stops)
- Cancel previous fetch requests when a new search starts
- Handle network errors gracefully
Let begin step by step
1. Listen and Debounce input
Add a HTML element to listen
<input type="text" id="searchInput" placeholder="Type to search..." />
Lets create debounce functionality. Debounce waits a certain amount of time after the last keystroke.To create a debounce functionality we can create a timer when input is received and clear the old timer
function debounce(fn, delayMs) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delayMs);
};
}
Listen to the HTML element and perform debounce behavior and API call fetching
const debouncedSearch = debounce(fetchData, 350); // ← adjust delay to taste
// Event listener
searchInput.addEventListener('input', (e) => {
const query = e.target.value;
debouncedSearch(query);
});
2. Cancel previous fetch requests when a new search starts
To achieve the behavior of cancelling the previous request, we will make use of AbortController. It helps in signaling the fetch request to cancel.
Create a new AbortController which has a property called signal (which is an instance of AbortSignal)
let currentAbortController = null; // To track & abort previous requests
currentAbortController = new AbortController();
const signal = currentAbortController.signal;
Perform API call and provide the AbortController's signal
const response = await fetch(
`https://jsonplaceholder.typicode.com/posts?q=${encodeURIComponent(
query
)}`,
{
method: 'GET',
mode: 'cors', // Explicit for CORS compatibility
signal: signal, // For aborting
}
);
To cancel the API call when new input is provided we need to call AbortController's abort method.
When you call .abort(): The signal.aborted boolean switches from false to true. An abort event is fired on the signal object.
// Abort previous request if exists
if (currentAbortController) {
currentAbortController.abort();
}
Then the browser "Unplugs" the request
If you passed that signal to a fetch() call, the browser is "listening" to that signal. As soon as the signal changes:
The HTTP Connection: The browser immediately stops the data transfer. If the request was still uploading or downloading bytes, the browser closes the TCP socket or stops the stream.
Network Activity: If you look at the Network Tab in your browser's Developer Tools, you will see the status of the request change to (canceled).
In your JavaScript code, the fetch() promise (which was "pending") is immediately rejected.
It throws a specific error: DOMException with the name **AbortError**.
3. Handle network errors gracefully
We need to add try/catch logic as AbortError can help trigger the .catch() block
try {
// fetch data logic ....
// handle http errors
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const data = await response.json();
// handle logic using data ....
} catch (error) {
if (error.name !== 'AbortError') {
// Ignore abort errors
}
} finally {
currentAbortController = null; // Reset after completion
}
Make sure to abort previous controller before creating new one.
// Function to make API call
async function fetchData(query) {
// Abort previous request if exists
if (currentAbortController) {
currentAbortController.abort();
}
// Create new AbortController for this request
currentAbortController = new AbortController();
const signal = currentAbortController.signal;
// fetch data logic...
}
Code Playground
Using this approach only the last meaningful request completes and updates the UI. No dependencies, no build step, works in every modern browser.
Thanks for reading. Happy coding!!!
Top comments (0)