en.javascript.info/5-network/04-fetch-abort/article.md
2022-04-13 11:44:21 +01:00

148 lines
4.5 KiB
Markdown

# Fetch: Abort
As we know, `fetch` returns a promise. And JavaScript generally has no concept of "aborting" a promise. So how can we cancel an ongoing `fetch`? E.g. if the user actions on our site indicate that the `fetch` isn't needed any more.
There's a special built-in object for such purposes: `AbortController`. It can be used to abort not only `fetch`, but other asynchronous tasks as well.
The usage is very straightforward:
## The AbortController object
Create a controller:
```js
let controller = new AbortController();
```
A controller is an extremely simple object.
- It has a single method `abort()`,
- And a single property `signal` that allows to set event listeners on it.
When `abort()` is called:
- `controller.signal` emits the `"abort"` event.
- `controller.signal.aborted` property becomes `true`.
Generally, we have two parties in the process:
1. The one that performs a cancelable operation, it sets a listener on `controller.signal`.
2. The one that cancels: it calls `controller.abort()` when needed.
Here's the full example (without `fetch` yet):
```js run
let controller = new AbortController();
let signal = controller.signal;
// The party that performs a cancelable operation
// gets the "signal" object
// and sets the listener to trigger when controller.abort() is called
signal.addEventListener('abort', () => alert("abort!"));
// The other party, that cancels (at any point later):
controller.abort(); // abort!
// The event triggers and signal.aborted becomes true
alert(signal.aborted); // true
```
As we can see, `AbortController` is just a mean to pass `abort` events when `abort()` is called on it.
We could implement the same kind of event listening in our code on our own, without the `AbortController` object.
But what's valuable is that `fetch` knows how to work with the `AbortController` object. It's integrated in it.
## Using with fetch
To be able to cancel `fetch`, pass the `signal` property of an `AbortController` as a `fetch` option:
```js
let controller = new AbortController();
fetch(url, {
signal: controller.signal
});
```
The `fetch` method knows how to work with `AbortController`. It will listen to `abort` events on `signal`.
Now, to abort, call `controller.abort()`:
```js
controller.abort();
```
We're done: `fetch` gets the event from `signal` and aborts the request.
When a fetch is aborted, its promise rejects with an error `AbortError`, so we should handle it, e.g. in `try..catch`.
Here's the full example with `fetch` aborted after 1 second:
```js run async
// abort in 1 second
let controller = new AbortController();
setTimeout(() => controller.abort(), 1000);
try {
let response = await fetch('/article/fetch-abort/demo/hang', {
signal: controller.signal
});
} catch(err) {
if (err.name == 'AbortError') { // handle abort()
alert("Aborted!");
} else {
throw err;
}
}
```
## AbortController is scalable
`AbortController` is scalable. It allows to cancel multiple fetches at once.
Here's a sketch of code that fetches many `urls` in parallel, and uses a single controller to abort them all:
```js
let urls = [...]; // a list of urls to fetch in parallel
let controller = new AbortController();
// an array of fetch promises
let fetchJobs = urls.map(url => fetch(url, {
signal: controller.signal
}));
let results = await Promise.all(fetchJobs);
// if controller.abort() is called from anywhere,
// it aborts all fetches
```
If we have our own asynchronous tasks, different from `fetch`, we can use a single `AbortController` to stop those, together with fetches.
We just need to listen to its `abort` event in our tasks:
```js
let urls = [...];
let controller = new AbortController();
let ourJob = new Promise((resolve, reject) => { // our task
...
controller.signal.addEventListener('abort', reject);
});
let fetchJobs = urls.map(url => fetch(url, { // fetches
signal: controller.signal
}));
// Wait for fetches and our task in parallel
let results = await Promise.all([...fetchJobs, ourJob]);
// if controller.abort() is called from anywhere,
// it aborts all fetches and ourJob
```
## Summary
- `AbortController` is a simple object that generates an `abort` event on its `signal` property when the `abort()` method is called (and also sets `signal.aborted` to `true`).
- `fetch` integrates with it: we pass the `signal` property as the option, and then `fetch` listens to it, so it's possible to abort the `fetch`.
- We can use `AbortController` in our code. The "call `abort()`" -> "listen to `abort` event" interaction is simple and universal. We can use it even without `fetch`.