JavaScript
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax and Types
-
Section 3: Strings and Numbers in Depth
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Objects and Arrays
-
Section 7: Maps, Sets, and Symbols
-
Section 8: Asynchronous JavaScript
-
Section 9: Object-Oriented and Prototypes
-
Section 10: The DOM
-
Section 11: Browser APIs
-
Section 12: Modern JavaScript (ES2015-ES2025)
-
Section 13: Functional Programming Patterns
-
Section 14: Error Handling and Debugging
-
Section 15: Testing
-
Section 16: Accessibility for JavaScript Developers
-
Section 17: Internationalization and Localization
-
Section 18: Performance
-
Section 19: Node.js Fundamentals
-
Section 20: Regular Expressions
-
Section 21: Design Patterns in JavaScript
-
Section 22: Security Basics
-
Section 23: Data Structures and Algorithms in JavaScript
-
Section 24: Practical Projects
-
Section 25: More Advanced Async Patterns
-
Section 26: More Object and Class Practice
-
Section 27: Working with Dates and Internationalization
-
Section 28: Web Components
-
Section 29: More DOM and Browser Practice
-
Section 30: Build Tooling for Vanilla JavaScript
-
Section 31: More Practice Projects
-
Section 32: Interview and Algorithm Practice
-
Section 33: Error Objects (MDN Reference)
-
Section 34: TypedArrays and Binary Data
-
Section 35: Reflection and Metaprogramming (MDN Reference)
-
Section 36: More Global Functions (MDN Reference)
196: Cancellable Async Workflows
I've seen this bug more times than I can count in production code: a user types quickly into a search box, the UI flickers, and suddenly the results on the screen don't match what's in the input field. It's a classic race condition. You fire off an async request for "a", then "ap", then "app", and because the network is unpredictable, the request for "a" happens to finish last. It overwrites the more specific results, and your user is left wondering why your app is broken.
The Race Condition Trap
The naive way to handle this is to just let the promises resolve and hope for the best, or perhaps try to keep track of a "request ID" manually. You might write something like this:
async function handleSearch(query) {
const results = await fetch(`/api/search?q=${query}`).then(res => res.json());
updateUI(results);
}
The problem here is that handleSearch has no way to tell a previous execution, "Hey, I'm the new priority, you can stop now." Once that fetch is in flight, it's a runaway train. Even if you try to use a global variable to track the "latest" request ID, the browser is still downloading the data and the JavaScript engine is still processing the promise resolution. You're wasting bandwidth and CPU cycles on data the user no longer cares about.
The Signal to Stop
To fix this, we need a way to communicate a cancellation signal from the outside into the async operation. In modern JavaScript, that's exactly what the AbortController is for. I like to think of it as a remote kill-switch for any web request.
Instead of just calling fetch, you create a controller and pass its signal to the request. If you call abort() on that controller, the browser immediately cancels the network request. Here is how I'd restructure that search logic:
let searchController = null;
async function handleSearch(query) {
// If there's a pending request, kill it before starting a new one
if (searchController) {
searchController.abort();
}
searchController = new AbortController();
const { signal } = searchController;
try {
const response = await fetch(`/api/search?q=${query}`, { signal });
const results = await response.json();
updateUI(results);
} catch (err) {
if (err.name === 'AbortError') {
// We intentionally cancelled this, so we can just ignore it.
return;
}
// Handle actual network errors here
console.error('Search failed:', err);
}
}
Handling the Aftermath
There is one important detail here that often trips people up: when you call abort(), the promise doesn't just vanish into thin air. It rejects with a specific error called an AbortError. If you don't wrap your await in a try/catch block, you'll end up with an "Uncaught (in promise)" error in your console every single time a user types a character.
You have to decide if a cancellation is a "failure" or a "normal event." In the case of a search-as-you-type feature, it's a normal event. I always filter for err.name === 'AbortError' and return early. This keeps the console clean and ensures that your actual error handling logic (like showing a "Network Error" toast) only triggers when something actually went wrong.
This pattern isn't just for fetch, either. You can pass that same signal into other custom async functions you write. If you have a heavy loop processing data, you can periodically check if (signal.aborted) return; to stop the work mid-stream. It's a powerful way to keep your application responsive and your network tab from looking like a disaster zone.
📋 Practical Task
Implement a Cancellable Typeahead Search
Create a small search interface consisting of an <input> and a <div id="results">. Implement a function that fetches data from the JSONPlaceholder API (https://jsonplaceholder.typicode.com/posts?q=QUERY) as the user types.
- Use an
AbortControllerto ensure that if the user types a new character before the previous request completes, the previous request is cancelled. - Ensure that
AbortErrorexceptions are caught and ignored so they don't clutter the console. - Update the
#resultsdiv with the titles of the posts returned by the API. - Verify in the Network tab of your browser's DevTools that previous requests are marked as "(canceled)" when you type quickly.
There are no comments for now.