May 13, 2025

Rethinking Cancellation in Frontend Engineering

Extra complexity for minimal gain

Many frontend engineers, myself included, viewed cancellation as an afterthought. “It’s just a network request, what’s the harm of letting it finish?” This perspective, while understandable, glosses over the implications of having a staling async operations running.

Frankly, cancellation seemed like an unnecessary complication. After all, the request usually reaches the server, and cancelling it on the client doesn’t magically halt the server’s processing. Plus, adding cancellation logic meant another branch of code to manage, another potential source of bug. It felt like adding complexity for minimal gain.

Looking deeper

However, my simplistic view started to shift once I look deeper. It became clear that dismissing cancellation as “nice-to-have” was a just an excuse for my ignorance. Our frontend application often does more than fetch data and display it: subsequent processing, data manipulations, or state updates. A stray, uncancelled request becomes a source of unpredictability, leading to race conditions, inconsistent UI states, and overall application instability.

Consider a scenario where a user rapidly switches between tabs or typing search queries. Without proper cancellation, multiple requests might be fired off, and may be completed in any order. Each response then competes to update the UI, potentially leading to flickering, outdated data, and a confusing experience.

Unpacking cancellation

Let’s discuss few natures of cancellation that may not be obvious at first glance.

Cancellation is an act of intent

We aim to stop an operation, but sometimes it’s simply too late. The request might already be beyond the point of return, or the server might be too far along its process. It’s about doing our best to clean up and prevent unnecessary work, not about guaranteed success.

States in an async operation

If we look at the typical states of an async operation above, we can see that cancellation is only possible in the “Pending” state. Once the operation is in “Success” or “Error” state, it’s too late.

Cancellation in JavaScript is usually linked to asynchronous operations

Being a single threaded environment, JavaScript runtime execute synchronous operations in a blocking manner. This means that once a synchronous function or block of code begins, it runs to completion without yielding control back to the caller or any other process until it has finished its work. There is no “gap” for the process to check for a cancellation signal and be interrupted.1

It’s in the gaps between asynchronous operations, those moments where the code wait for a response, that we have the opportunity to check if the caller has the intention to cancel the operation.

Cancellation is more than just that async operation

It’s about cancelling the chain of events that follow. If a network request triggers subsequent processing or updates, cancelling the request also stop those subsequent operations.

Say, the following code:

async function showDashboard() {
  const data = await fetchDashboardData();
  const widgets = await fetchWidgets();

  await displayDashboard(data, widgets);

  pollForUpdates();
}

If the fetchDashboardData or fetchWidgets operation is cancelled, even if the network requests are already received by the server, we can still skip the displayDashboard and pollForUpdates operations.

This broader perspective turns cancellation from a minor async optimization into a key component of ensuring a consistent and reliable user experience.

Incorporating cancellation into your code

The beauty of incorporating cancellation is that it doesn’t necessarily require a complete overhaul of existing code. Most of us already use try ... catch blocks to handle errors of async operations. The key is to broaden our definition of what is being caught in the catch phrase. Instead of thinking it’s catching error, think of it as catching exception. Cancellation is just another exception to be handled.

With this view, cancellation is less of an alien concept and more of an extension of our existing error handling.

  • You can handle the cancellation in the catch block.
  • If you do not want to handle the cancellation, you can let it propagate, just like normal error handling.

Let’s dive into the details of how to implement cancellation in your code.

Standardized cancellation

JavaScript has two built-in objects to standardize cancellation mechanism: AbortController and AbortSignal:

const abortController = new AbortController();

// How to abort the operation
// You can pass the cancellation reason, which is defaulted to `AbortError`.
abortController.abort();

JS API that supports cancellation

You can pass the AbortSignal to fetch and addEventListener to make them cancellable:

async function fetchDashboardData({ abortSignal }: { abortSignal?: AbortSignal }) {
  const response = await fetch('/dashboard-data', {
    signal: abortSignal,
  });

  if (!response.ok) {
    throw new Error('Failed to fetch dashboard data');
  }

  return response.json();
}

async function listenForVisibilityChange({ abortSignal }: { abortSignal?: AbortSignal }) {
  document.addEventListener(
    'visibilitychange',
    (ev) => {
      fetchDashboardData({});
    },
    {
      signal: abortSignal,
    }
  );
}

Many NodeJS API also start supporting AbortSignal as an optional parameter, e.g.

import { createReadStream } from 'node:fs';

const abortController = new AbortController();

const readStream = createReadStream('file.txt', {
  signal: abortController.signal,
});

abortController.abort();

Supporting cancellation in your code

It’s straightforward to support cancellation in your code if the underlying implementation supports it - you just need to start accepting AbortSignal as an optional parameter and pass it to the underlying implementation.

However, what if the underlying implementation doesn’t support cancellation?

In that case, you can’t cancel the operation, but you can stop the chain of events that follow.

As an example, consider the following snippet:


async function readClipboardText() {
  return navigator.clipboard.readText();
}

async function showClipboardText() {
  const text = await readClipboardText();
  await displayText(text);
}

We want to support cancellation for both readClipboardText and showClipboardText functions.

To do that, we can start by accepting AbortSignal as an optional parameter in both functions:

async function readClipboardText({ abortSignal }: { abortSignal?: AbortSignal }) {
  return navigator.clipboard.readText();
}

async function showClipboardText(options: { abortSignal?: AbortSignal }) {
  const text = await readClipboardText(options);
  await displayText(text);
}

Then, we can use the AbortSignal in readClipboardText:

async function readClipboardText({ abortSignal }: { abortSignal?: AbortSignal }) {
  abortSignal?.throwIfAborted();

  return new Promise<string>((fulfill, reject) => {
    abortSignal?.addEventListener('abort', () => reject(abortSignal.reason));

    navigator.clipboard.readText().then(fulfill).catch(reject);
  });
}

Note that even though navigator.clipboard.readText could not be cancelled, we can still cancel the chain of events that follow (execute displayText) if the AbortSignal is aborted before readClipboardText is completed.

Conclusion

Cancellation in frontend engineering is more than just stopping network requests - it’s about maintaining control over your application’s state and user experience. While it may seem like an optional optimization, proper cancellation handling is essential for preventing race conditions, reducing unnecessary processing, and ensuring UI consistency. The good news is that modern JavaScript provides robust tools like AbortController and AbortSignal that make implementing cancellation straightforward. By treating cancellation as a first-class concern alongside error handling, you can build more reliable and responsive applications without significantly increasing code complexity.

Footnotes

  1. Technically, you can make a synchronous function cancellable by checking for the cancellation signal within the function. However, it is seldom done in practice because checking for cancellation signal introduces additional overhead.

Thanks for reading!

Love what you're reading? Sign up for my newsletter and stay up-to-date with my latest contents and projects.

    I won't send you spam or use it for other purposes.

    Unsubscribe at any time.