#tech
#promise
#async
#javascript
3 min read

How to Cancel Promise with AbortController

Andriy Obrizan

You probably know that fetch can take an AbortSignal object that lets you cancel it anytime.

This DOM Standard API is deliberately generic by design to work with other APIs and custom JavaScript code. Many APIs require an abort mechanism that’s missing from the language. Before the standard, every library had to roll their custom solutions to abort pending operations. Integration was more complicated, as developers had to wrap those mechanisms with adapters to make them work with each other.

Now, it’s preferred to use AbortController that provides an abort() method that triggers the aborted state of the AbortSignal object obtained from signal property. API’s that support cancellation can take the AbortSignal object, listen to an abort event and stop the operation. To make error handling much simpler, those APIs are encouraged to reject the promise with "AbortError" DOMException.

Usage is pretty similar to fetch:

const controller = new AbortController();

// invoke controller.abort() in some callback

try{
  const result = await somethingAsync({signal: controller.signal});
  console.log(result);
}
catch (e){
  console.warn(e.name === "AbortError" ? "Promise Aborted" : "Promise Rejected");
}

Now in your async function, you can react to abort event and stop doing whatever it was doing:

function somethingAsync({signal}){
  if (signal?.aborted){
    return Promise.reject(new DOMException("Aborted", "AbortError"));
  }

  return new Promise((resolve, reject) => {
    console.log("Promise Started");

    let timeout;

    const abortHandler = () => {
      clearTimeout(timeout);
      reject(new DOMException("Aborted", "AbortError"));
    }

    // start async operation
    timeout = setTimeout(() => {
      resolve("Promise Resolved");
      signal?.removeEventListener("abort", abortHandler);
    }, 1000);    

    signal?.addEventListener("abort", abortHandler);
  });
}

We made the signal optional, as it’s only necessary for the cancellation support. Not all API use cases would need that, so you shouldn’t force the developers to create dummy AbortController objects only to pass the signal. Also, after the operation is completed successfully, we explicitly remove the listener to avoid memory leaks and other weird behavior with long-lived AbortController objects.

Now you can call controller.abort()anywhere you like to cancel the promise.

The most significant advantage over custom solutions is the ability to combine async code with cancellation support easily:

async function doLotsOfStuff({signal}){
  const resp1 = await fetch(url1, {signal});
  //... 
  const something = await somethingAsync({signal});
  //...
  const resp2 = await fetch(url2, {signal});
  //...
}

When all of the lower-level async functions leverage the exact abort mechanism, high-level code doesn’t necessarily have to deal with listening for abort events directly and can only pass the signal to other functions. They will throw the "AbortError" exception that will propagate and cancel the whole operation.

Conclusion

Embedded content was blocked by your Privacy Preferences. Feel free to view it externally.

AbortController provides convenient way to introduce cancellation support in every API that needs it. Having it standardized makes usage in business logic code much more straightforward, eliminating the need to consider low-level details.

According to caniuse, more than 92% of users have AbortController support. All major browsers implemented it a long time ago.

Unfortunately, it’s part of the DOM standard and not the ECMAScript spec, which means not every JavaScript runtime will have it. AbortController was implemented in NodeJS v15.0.0. Since v15.4.0 it’s no longer experimental and available to all applications by default.