Blog

Cancel Async Work in JavaScript With AbortController

A promise has no cancel method. Once started, it runs to completion and you either use the result or ignore it.

AbortController is the standard way around that. It gives you a signal you pass into the work, and a way to raise it later. fetch accepts one, and so can your own functions.

Make your own function abortable

The pattern is always the same. Check whether the signal is already aborted, then listen for the abort event and reject.

const sleep = (ms, signal) => new Promise((resolve, reject) => {
  if (signal?.aborted) return reject(signal.reason);
  const id = setTimeout(resolve, ms);
  signal?.addEventListener('abort', () => {
    clearTimeout(id);
    reject(signal.reason);
  }, { once: true });
});

// abort a pending operation
const controller = new AbortController();
setTimeout(() => controller.abort(), 20);
try { await sleep(1000, controller.signal); }
catch (err) { console.log(err.constructor.name + ':', err.message); }

It prints:

DOMException: This operation was aborted

{ once: true } removes the listener after it fires, which matters when one signal is shared by many operations.

A signal is one way

Once aborted, a signal stays aborted. It carries the reason with it.

// the signal remembers
console.log('aborted ->', controller.signal.aborted);
console.log('reason  ->', controller.signal.reason.name);

// aborting after the fact does nothing
await sleep(5);
console.log('still aborted ->', controller.signal.aborted);

It prints:

aborted -> true
reason  -> AbortError
still aborted -> true

There is no reset. A controller is for one operation or one batch, and you make a new one next time.

Your own reason

abort() takes an optional reason, which becomes the rejection value.

// a custom reason
const c2 = new AbortController();
c2.abort(new Error('user closed the tab'));
try { await sleep(50, c2.signal); }
catch (err) { console.log(err.message); }

It prints:

user closed the tab

Without an argument the reason is a DOMException named AbortError, which is the value you check for when you want to ignore cancellations rather than report them.

One signal, many operations

Every operation given the same signal is cancelled together.

// one signal cancels many operations
const c3 = new AbortController();
setTimeout(() => c3.abort(), 20);
const jobs = [sleep(1000, c3.signal), sleep(2000, c3.signal), sleep(3000, c3.signal)];
const results = await Promise.allSettled(jobs);
console.log(results.map(r => r.status));

It prints:

[ 'rejected', 'rejected', 'rejected' ]

This is how you tear down all the work belonging to a screen or a request in one call.

Timeouts and combining

AbortSignal.timeout creates a signal that aborts itself. AbortSignal.any aborts as soon as any of its inputs does.

// built-in timeout signal
try { await sleep(1000, AbortSignal.timeout(30)); }
catch (err) { console.log(err.name + ':', err.message); }

// combine signals
const manual = new AbortController();
const combined = AbortSignal.any([manual.signal, AbortSignal.timeout(5000)]);
setTimeout(() => manual.abort(), 20);
try { await sleep(1000, combined); }
catch (err) { console.log(err.name); }

It prints:

TimeoutError: The operation was aborted due to timeout
AbortError

The combined signal covers the usual requirement: stop when the user cancels, or after a time limit, whichever comes first. Note the different error names, which let you tell a timeout apart from a user cancellation.

What to remember

  • Pass signal into the work, and reject when it fires.
  • Check signal.aborted first, because it may already be raised.
  • A signal cannot be reset. Create a new controller each time.
  • AbortSignal.timeout and AbortSignal.any cover deadlines and combined cancellation.

The same signal object is accepted by fetch, by Node stream helpers and by addEventListener, so one controller can shut down network calls, listeners and your own async code together.

How useful was this post?

Click on a heart to rate it!

Average rating 0 / 5. Vote count: 0

No votes so far! Be the first to rate this post.

Leave a Reply