AbortController and AbortSignal

AbortController and AbortSignal

·

4 min read

Abort a web request

AbortController can be used along with the fetch api to abort a web request.

By creating an AbortController object, we could get a abort signal object. When the fetch request is initiated, we pass the abort signal object to the request's optionis object. Then we can call the abort function to abort the request at specified time.

const abortController = new AbortController();

fetch("https://yaox023.com/vocab/vite.svg", {
    signal: abortController.signal
})
    .then(response => response.text())
    .then(text => console.log(text))
    .catch(err => {
        console.log(err);
        // DOMException: The user aborted a request.
    });

setTimeout(() => {
    // abort after 10ms
    abortController.abort();
}, 10);

Abort multiple fetch

We can use the same abort signal object to abort multiple request.

const abortController = new AbortController();

function request() {
    fetch("https://yaox023.com/vocab/vite.svg", {
        // use the same signal
        signal: abortController.signal
    })
        .then(response => response.text())
        .then(text => console.log(text))
        .catch(err => {
            console.log(err);
            // DOMException: The user aborted a request.
        });
}

request();
request();
request();

setTimeout(() => {
    // abort after 10ms
    abortController.abort();
}, 10);

Remove event listener

Normally, if we want to remove an event listener, we need to remember the callback's reference and pass it to the removeEventListener function.

const btn = document.getElementById("btn")
const handleClick = () => {
    console.log("click");
    btn.removeEventListener("click", handleClick)
};
btn.addEventListener("click", handleClick)

We can also achieve this by using abort signal.

const abortController = new AbortController();

const btn = document.getElementById("btn");
btn.addEventListener(
    "click",
    () => {
        console.log("click");
        abortController.abort();
    },
    { signal: abortController.signal }
);

As you can see, in this way, we don't need to remember the callback reference, just call abort function is enough.

Abort api in node.js

In node.js, a lot of apis provide this abort signal option. For example, the readFile api in fs module, we can use this abort signal to abort the asynchronous file reading process.


const fs = require("fs").promises;

const abortController = new AbortController();
fs.readFile("./1.html", {
    signal: abortController.signal,
    encoding: "utf-8"
})
    .then(text => console.log(text))
    .catch(e => console.error(e)); 
    // AbortError: The operation was aborted

setTimeout(() => {
    abortController.abort();
}, 1);

Reason

We can pass the reason into the abort function, so later we can get the reason from abort error.

const fs = require("fs").promises;

const abortController = new AbortController();
fs.readFile("./1.html", {
    signal: abortController.signal,
    encoding: "utf-8"
})
    .then(text => console.log(text))
    .catch(e => console.error(e.cause)); 
    // my abort reason

setTimeout(() => {
    abortController.abort("my abort reason");
}, 0);

Timeout

In above example, we use setTimeout to abort the request. If we timeout value is fixed, we can use the static method timeout to create the signal so we don't need to abort manually.

const signal = AbortSignal.timeout(100);

fetch("https://yaox023.com/vocab/vite.svg", {
    signal
})
    .then(response => response.text())
    .then(text => console.log(text))
    .catch(err => {
        console.log(err);
    });

Abort event

The signal object has an abort event, which will be fired when the signal is aborted.

const signal = AbortSignal.timeout(100);
signal.addEventListener('abort', function (e) {
    console.log('signal is aborted');
});

fetch("https://yaox023.com/vocab/vite.svg", {
    signal
})
    .then(response => response.text())
    .then(text => console.log(text))
    .catch(err => {
        console.log(err);
    });

The timeout function and this abort event features are very useful. I talked about some techinque to run js functions in background in Running JavaScript in "Background". This abort feature can be another technique to achieve the same effect.

function interval(cb, timeout) {

    let stop = false;
    function inner() {
        const signal = AbortSignal.timeout(timeout);
        signal.addEventListener('abort', () => {
            cb();
            if (!stop) {
                inner();
            }
        });
    }
    inner();

    return () => {
        stop = true
    }
}

const base = Date.now();
const cb = () => console.log(Date.now() - base);
const clear = interval(cb, 1000)

throwIfAborted

Just as its name shows, this function will check if the signal object is already aborted. If it is, then throw an error.

For example, if we want to poll for a condition, and we want to have control to stop this polling at certain checkpoint, we create a function as below. Code copied from this.

async function waitForCondition(func, targetValue, { signal } = {}) {
  while (true) {
    signal?.throwIfAborted();

    const result = await func();
    if (result === targetValue) {
      return;
    }
  }
}