Node.js

Using AbortSignal in Node.js

Dave Clements is an open source advocate and is the tech lead and primary author of OpenJS Foundation Node.js training and certification programs.

Using AbortSignal in Node.js

By: James Snell, originally published on Nearform July 22, 2021

Foreword by: David Mark Clements

Dave Clements is an open source advocate and is the tech lead and primary author of OpenJS Foundation Node.js training and certification programs. And a big thank you to Nearform and James Snell for allowing the OpenJS Foundation to repost this article.

Foreword

The OpenJS Node Application Developer certification is an evergreen program that stays up to date
with advancements in the JavaScript specification, Node.js core, industry trends, and best practices
not only to ensure that the examination and training stay relevant but also to help disseminate
important information for the Node & JavaScript community.

With that in mind, the following article by James Snell is republished with permission from
James and NearForm where the article was first published. We strongly recommend anyone thinking
of taking the JSNAD certification read this article and consider the implications. We hope you
enjoy it!

The AbortController and AbortSignal APIs are quickly becoming the standard mechanism for canceling asynchronous operations in the Node.js core API.

If you search how to use the Promise.race() API, you’ll come across quite a few variations of the following:

The intent here is straightforward: Start a potentially long-running task but trigger a timeout if that task takes too long to complete. This is generally a good idea, but there are quite a few problems with this common example.

First, although the promise returned by Promise.race() will be fulfilled as soon as the first of the given promises is settled, the other promises are not cancelled and will keep on running. Although the timeout timer did fire, the long-running task is never actually interrupted and stopped.

Second, what happens to the timeout promise if the long-running task completes before the timeout is triggered? The answer is simple: The timer keeps running, and the promise will end up rejecting, still with an unhandled rejection — unnecessarily risking performance issues and possible memory leaks in your application.

To correctly handle this pattern, we need a reliable mechanism for signalling across the two promises, canceling either the timer or the long-running task as appropriate and ensuring that once the timeout is triggered all resources are cleaned up as quickly as possible. Fortunately, Web Platform APIs provide a standard mechanism for this kind of signalling — the AbortController and AbortSignal APIs.

In Node.js, a better way if implementing a Promise.race-based timeout would be:

As with the previous example, two promises are created. However, when each completes, it uses the AbortController and AbortSignal APIs to explicitly signal to the other that it should stop. As long as the code in those is written to support the AbortSignal API, everything just works.

For instance, in the example we make use of the recently added awaitable timers API in Node.js. These are variants of the setTimeout() and setInterval() that return promises.

The awaitable timer API supports the ability to pass in an AbortSignal instance. When the AbortSignal is triggered, the timer is cleared and the promise immediately rejects with an AbortError.

Support for AbortController and AbortSignal is being rolled out across the Node.js core API and can now be found in most of the major subsystems. Before we explore where the API can be used, let’s find out a bit more about the API itself.

All about AbortController and AbortSignal

The AbortController interface is simple. It exposes just two important things — a signal property whose value is an AbortSignal and an abort() method that triggers that AbortSignal.

The AbortSignal itself is really nothing more than an EventTarget with a single type of event that it emits — the ‘abort’ event. One additional boolean aborted property is true if the AbortSignal has already been triggered:

The AbortSignal can only be triggered once.

Notice that when I added the event listener in the example above, I included the { once: true } option. This ensures that the event listener is removed from the AbortSignal as soon as the abort event is triggered, preventing a possible memory leak.

Note that it’s even possible to pass an AbortSignal onto the addEventListener() itself, causing the event listener to be removed if that AbortSignal is triggered.

This starts to get a bit complicated too, but it’s important for preventing memory leaks when coordinating the cancellation of multiple complex tasks. We’ll see an example of how this all comes together next.

Implementing API support for AbortSignal

The AbortController API is used to signal that an operation should be cancelled. The AbortSignal API is used to receive notification of those signals. They always come in pairs.

The idiomatic way of enabling a function (like the someLongRunningTask() function in our examples above) to support this pattern is to pass a reference to the AbortSignal in as part of an options object:

Within this function, you should immediately check to see if the signal has already been triggered and, if it has, immediately abort the operation.

Next, it’s important to set up the handling of the ‘abort’ event before starting to process the task:

Notice here tha

t we are creating an additional AbortController instance whose signal is passed in with the event listener. After we’ve completed the asynchronous task, we trigger that AbortController to let the AbortSignal know that the event handler can be removed. We want to make sure that the listener is cleaned up even if the async task fails, so we wrap the call to taskDone.abort() in a finally block.

It is also important to check if the signal has been triggered between various async tasks the method may be performing. This is important to catch cases where the event may not yet have had an opportunity to be emitted but the operation should still be interrupted.

Using AbortController and AbortSignal

The AbortController and AbortSignal APIs are quickly becoming the standard mechanism for canceling asynchronous operations in the Node.js core API.
For example, as of node.js 15.3.0, it is possible to cancel an HTTP request using the API:

Consult the Node.js documentation for more details on exactly which APIs support AbortSignal. More are being added all the time and support may vary across different Node.js major versions.