How to cancel an HTTP request in Node.js
5 min read
If you’re making an HTTP request in Node.js there’s a good chance you’ll want to cancel it if it takes too long to receive a response. Or perhaps you have a slightly more complex situation where you’re making multiple requests in parallel, and if one request fails you want to cancel all of them. These sound like reasonable things to want to do, but solving these problems is often much less straightforward. You might even end up with a hacky workaround involving setTimeout()
(it’s ok, I’ve been there too!).
Fortunately there’s a JavaScript API which gives us a standard way to cancel asynchronous tasks such as an in-flight HTTP request: the Abort API. This small but powerful API has been available in some web browsers since 2017, and now it’s available in Node.js too. Let’s take a look at what the Abort API is, how it works, and how we can use it to cancel an HTTP request in Node.js.
The Abort API is a JavaScript API which consists of two classes: AbortController
and AbortSignal
. Here’s an example of them in action:
const controller = new AbortController();
const signal = controller.signal; signal.addEventListener("abort", () => { console.log("The abort signal was triggered");
}, { once: true }); controller.abort();
When the controller.abort()
method is called, it sets the signal’s aborted
property to true
. It also dispatches an abort
event from the AbortSignal
instance in controller.signal
to the list of registered handlers. In the example above we’ve registered one handler for the abort
event type, which logs a message to let us know that the abort signal was triggered. If you’re using a library or method which accepts an AbortSignal
instance as an option, it will attach it’s own event handler for the abort
event.
The Abort API originated in the Web Platform. Microsoft Edge 16 was the first browser to implement the Abort API in October 2017, but now all major browsers support it.
Node.js has had Stable support for the Abort API since v15.4.0, released in December 2020. It was backported to Node.js v14.17.0, but with an Experimental status. If you’re using Node.js v14.x or an earlier version of Node.js you’ll probably want to use the abort-controller library (the Node.js core implementation was closely modelled on this library).
You can learn about support for the Abort API in different versions of Node.js in my article ‘Cancel async operations with AbortController‘.
The following code examples require Node.js v16.0.0 or greater as they use the promise variant of
setTimeout()
.
Now let’s take what we’ve learnt about the Abort API and use it to cancel an HTTP request after a specific amount of time. We’re going to use the promise variant of setTimeout() as it accepts an AbortSignal
instance via a signal
option. We’re also going to pass another AbortSignal
as an option to a function which makes an HTTP request. We’ll then use Promise.race()
to "race" the timeout and the HTTP request against each other.
First, we’ll install a popular implementation of the Fetch API for Node.js, node-fetch:
npm install node-fetch
Then we’re going to import the modules which we’ll be using:
import fetch from "node-fetch"; import { setTimeout } from "node:timers/promises";
We need to import the promise variant of
setTimeout()
from thetimers/promises
module as it’s not a global like the callbacksetTimeout()
function.
Now we’ll create two AbortController
instances:
const cancelRequest = new AbortController();
const cancelTimeout = new AbortController();
Next we’ll create a makeRequest()
function which uses node-fetch
to make an HTTP request:
async function makeRequest(url) { try { const response = await fetch(url, { signal: cancelRequest.signal }); const responseData = await response.json(); return responseData; } finally { cancelTimeout.abort(); }
}
There are two things to note in the code snippet above:
- We’re passing
cancelRequest.signal
(anAbortSignal
instance) as asignal
option to thefetch()
function. This will allow us to abort the HTTP request whichfetch()
makes. - We’re calling the
abort()
method on ourcancelTimeout
abort controller when our request has completed. We’ll see why in just a moment.
Now we’ll create a timeout()
function which will throw an error after a specified amount of time:
async function timeout(delay) { try { await setTimeout(delay, undefined, { signal: cancelTimeout.signal }); cancelRequest.abort(); } catch (error) { return; } throw new Error(`Request aborted as it took longer than ${delay}ms`);
}
The promise variant of setTimeout()
which we’re using here works quite differently to its better known callback based counterpart. The arguments it accepts are:
- The number of milliseconds to wait before fulfilling the promise.
- The value with which to fulfill the promise. In this case it’s
undefined
as we don’t need a value here – we’re usingsetTimeout()
to delay the execution of the code which comes after it. - An options object. We’re passing the
signal
option here, with theAbortSignal
in ourcancelRequest
abort controller. This allows us to cancel the scheduled timeout by calling thecancelTimeout.abort()
method. We do this in themakeRequest()
function above after our HTTP request has completed.
If the timeout completes without being aborted, we call the abort()
method on our cancelRequest
controller. We then throw an error with an error message which explains that the request was aborted.
We’ve wrapped the setTimeout()
call in a try
/ catch
block because if the cancelTimeout
signal is aborted, the promise returned by setTimeout()
rejects with an AbortError
. We don’t need to do anything with that error, but we do need to return
so that the function doesn’t continue to execute and throw our "request aborted" error.
After defining our makeRequest()
and timeout()
functions, we pull it all together with Promise.race():
const url = "https://jsonplaceholder.typicode.com/posts"; const result = await Promise.race([makeRequest(url), timeout(100)]); console.log({ result });
In the code snippet above we’re using the free fake API provided by JSON Placeholder.
Our call to Promise.race()
returns a promise that fulfills or rejects as soon as one of the promises in the array fulfills or rejects, with the value or reason from that promise. With the functions that we’re calling in the array, that means:
- If the HTTP request made by
makeRequest()
completes in under 100 milliseconds — as specified by the argument to ourtimeout()
function — the value ofresult
will be the parsed JSON response body. The timeout created bytimeout()
will also be aborted. If an error occurs when making the HTTP request, the promise returned byPromise.race()
will reject with that error. - If the HTTP request does not complete in under 100 milliseconds, the
timeout()
function will abort the HTTP request and throw an error which explains that the request was aborted.Promise.race()
will reject with that error.
You can view the full code which we’ve put together in this example on GitHub.
Thanks to James Snell for sharing the
Promise.race
-based timeout pattern in his article Using AbortSignal in Node.js.
Support is growing for the Abort API in Node.js compatible HTTP libraries, and in the Node.js core APIs too.
Libraries
- Node Fetch
- Undici
-
Axios. Accepts a
signal
option since v0.22.0 (released on Oct 1st 2021). You’ll find details on how to pass inAbortSignal
in the README, but not yet on the Axios docs website.
Node.js core API methods
The following HTTP related methods in Node.js accept an AbortSignal
as a signal
option:
I’m pretty sure we’re going to see a lot more of the Abort API as other libraries, as well as methods in Node.js core, add support for it. Combined with the promise variant of setTimeout
, it’s a powerful way to gain more control over your HTTP requests.
I’m looking forward to seeing new patterns emerge around the Abort API. If you’ve got some other ideas of ways it can be used, I’d love to hear about them. Post a comment below or drop me a message on Twitter.