2020-08-15
|~4 min read
|656 words
Update:
AbortController
is now shipping in Node, though currently under anexperimental
flag. The API is based on the web API described below. This is exciting as it brings the same capability to cancel Promises to the server!
With the introduction of the AbortController
, we now have the ability to cancel fetch
requests declaratively. This allows an early escape from a Promise
which does not have its own method for canceling (i.e. there’s no Promise.cancel()
to abort).
To make use of this, we’ll need a few pieces:
AbortController
instancesignal
property to the cancelable eventabort
with the instance methodA bare bones example might look like the following. Let’s imagine we want to get information about Pikachu from the pokeapi:
fetch("https://pokeapi.co/api/v2/pokemon/pikachu", { signal }).then((res) =>
console.log(res),
)
But then, we decide we actually want to abort that request:
const controller = new AbortController()
const { signal } = controller
fetch("https://pokeapi.co/api/v2/pokemon/pikachu", { signal })
.then((res) => console.log(res))
.catch((e) => console.log({ e, name: e.name }))
controller.abort()
The resulting error’s name is AbortError
with a message of "The user aborted a request."
This works because we’ve passed the signal
into the fetch
which acts as a listener for anytime an instance of the AbortController executes its abort
method. The signal
is an AbortSignal
, an object that facilitates communication with a DOM request.
What about adding more control? Instead of a blanket abort
, we can abort the fetch
if too much time has passed using setTimeout
to manage the execution. For example:
const fetchHandler = async (url, options) => {
const controller = new AbortController()
const { signal } = controller
const timeout = setTimeout(() => {
controller.abort()
}, 500)
try {
return await fetch(url, {
...options,
signal,
})
} catch (error) {
if (error.name === "AbortError") {
throw new Error(`Fetch canceled due to timeout`)
} else {
throw new Error(`Generic Error: ${error}`)
}
} finally {
clearTimeout(timeout)
}
}
This example is technically more verbose than it needs to be, but I like it for its explicitness.1 This also builds on the same principles we saw earlier, except now the abort
method is only called if 500 milliseconds elapse before we get to the finally
block. If we get there then the timeout is cleared and the abort
method will never execute.
While the AbortController
brought the ability to cancel to the fetch
API, we’ve had the ability cancel XHR requests for a while. The XMLHttpRequest
has an abort
method:
// source: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/abort
var xhr = new XMLHttpRequest(),
method = "GET",
url = "https://developer.mozilla.org/"
xhr.open(method, url, true)
xhr.send()
if (OH_NOES_WE_NEED_TO_CANCEL_RIGHT_NOW_OR_ELSE) {
xhr.abort()
}
Additionally libraries that facilitate requests, like Axios, have made canceling ergonomic for a while, offering two solutions:
Canceling with a cancelToken
// source: https://github.com/axios/axios#cancellation
const CancelToken = axios.CancelToken
const source = CancelToken.source()
axios
.get("/user/12345", {
cancelToken: source.token,
})
.catch(function (thrown) {
if (axios.isCancel(thrown)) {
console.log("Request canceled", thrown.message)
} else {
// handle error
}
})
axios.post(
"/user/12345",
{
name: "new name",
},
{
cancelToken: source.token,
},
)
// cancel the request (the message parameter is optional)
source.cancel("Operation canceled by the user.")
Cancel by passing an executor function to a CancelToken
constructor
// source: https://github.com/axios/axios#cancellation
const CancelToken = axios.CancelToken
let cancel
axios.get("/user/12345", {
cancelToken: new CancelToken(function executor(c) {
// An executor function receives a cancel function as a parameter
cancel = c
}),
})
// cancel the request
cancel()
Thank you to Jake Archibald, David Walsh, and MDN for their work in making cancelable fetch requests a reality (in the case of Jake) and understandable (all).
It’s ok to call .abort() after the fetch has already completed, fetch simply ignores it.
Hi there and thanks for reading! My name's Stephen. I live in Chicago with my wife, Kate, and dog, Finn. Want more? See about and get in touch!