Skip to Content
Course content

65: AbortController for Cancelling Requests

Click on the "Edit" button in the top corner of the screen to edit your slide content.

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 AbortController variable 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 setInterval that calls fetchDashboardData() every 5 seconds.
  • A button in the HTML with the id #refresh-btn that calls fetchDashboardData() immediately when clicked.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.