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)
65: AbortController for Cancelling Requests
Ever built a search autocomplete bar where you type quickly, and for a split second, the results for your first letter pop up after the results for your third letter? It's a jarring user experience, and it happens because network requests don't always finish in the order they were sent. We call this a race condition.
The "Ghost" Result Problem
I was working on a project recently where I had a simple input field fetching data from a JSON API. Here was my initial approach:
async function handleSearch(query) {
const response = await fetch(`https://api.example.com/search?q=${query}`);
const data = await response.json();
renderResults(data);
}
input.addEventListener('input', (e) => {
handleSearch(e.target.value);
});
At first glance, it looks fine. But if I type "JavaScript" rapidly, I'm firing ten different requests. If the request for "J" takes 2 seconds but the request for "JavaScript" takes 200ms, the "J" results will overwrite the "JavaScript" results. The UI ends up lying to the user. Now, I could try to keep track of a "latest request ID" and ignore old ones, but that's just hiding the problem. The browser is still downloading data we've already decided we don't want. That's a waste of bandwidth and memory.
Cutting the Connection
I wondered if there was a way to actually stop the browser from completing a request. That's where AbortController comes in. It's essentially a remote control for a fetch request. You create the controller, grab its "signal," and hand that signal to the fetch call.
Let's try modifying the logic so we kill the previous request before starting a new one:
let controller;
async function handleSearch(query) {
// If a previous request is still flying, kill it
if (controller) {
controller.abort();
}
// Create a new controller for the current request
controller = new AbortController();
const signal = controller.signal;
try {
const response = await fetch(`https://api.example.com/search?q=${query}`, { signal });
const data = await response.json();
renderResults(data);
} catch (err) {
// We'll deal with this in a second
console.error(err);
}
}
Now, every time the user types a character, the previous fetch is cancelled. If you open the Network tab in your DevTools, you'll actually see the status of those cancelled requests change to "(canceled)". This is much cleaner.
Cleaning Up the Noise
The moment I ran the code above, I noticed my console was filling up with DOMException: The user aborted a request.. This is technically an error, but in our case, it's an expected error. We told it to abort; we shouldn't be treating that as a system failure.
I need to differentiate between a real network failure (like the server being down) and a deliberate cancellation. I can do that by checking the name of the error object:
try {
const response = await fetch(`https://api.example.com/search?q=${query}`, { signal });
const data = await response.json();
renderResults(data);
} catch (err) {
if (err.name === 'AbortError') {
console.log('Fetch aborted: we found a newer request to handle.');
} else {
console.error('A real error occurred:', err);
}
}
Now the console is quiet, the bandwidth is saved, and the UI always shows the results for the most recent keystroke. It's a small addition, but it's the difference between a "prototype" and a professional-grade interface.
📋 Practical Task
Implementing a Cancellable Timer-Based Fetch
You are building a "Live Dashboard" that refreshes data every 5 seconds. However, if the user clicks a "Refresh Now" button, you want to immediately cancel the pending automatic refresh and start a new one to avoid duplicate overlapping requests.
Your task: Create a script that implements the following:
- A global
AbortControllervariable to track the current request. - A function
fetchDashboardData()that:- Aborts any existing controller if it exists.
- Creates a new
AbortController. - Fetches data from
https://jsonplaceholder.typicode.com/todos/1(using the signal). - Logs "Data updated!" on success.
- Catches errors and only logs "Request cancelled" if the error is an
AbortError.
- A
setIntervalthat callsfetchDashboardData()every 5 seconds. - A button in the HTML with the id
#refresh-btnthat callsfetchDashboardData()immediately when clicked.
There are no comments for now.