Euan's Blog

Implementing a cancellable asynchronous delay in JavaScript

While working on a React project recently, I had a need to update some state periodically with data from an API retrieved using fetch(). Coming from a C# background, the way I would approach this problem there would be something like the following:

private async Task FetchDataContinuouslyAsync(CancellationToken cancellationToken)
    while (!cancellationToken.IsCancellationRequested)
        await FetchDataAndSetStateAsync(cancellationToken);

        // now wait for 15 seconds before trying again
        await Task.Delay(15000, cancellationToken);

Naturally, I went to approach the problem the same way in JavaScript. That's where I hit a snag though - there's no built in function analogous to Task.Delay().

This meant I had to come up with my own solution to the problem. Searching the internet yielded plenty of results where people were using setTimeout along with a Promise, but surprisingly few which supported early cancellation - and those that did tend to return a cancel function rather than observing a token for cancellation. As I was already using fetch()`` with an [AbortController`]( to cancel requests, I wanted to re-use that controller for cancellation.

Here's what I came up with:

 * Return a promise that is resolved after a given delay, or after being cancelled.
 * @param  {number} duration The delay, in milliseconds.
 * @param  {AbortSignal|null} signal An optional AbortSignal to cancel the delay.
 * @return {Promise<void>} A promise that is either resolved after the delay, or rejected after the signal is cancelled.
function asyncSleep(duration, signal) {
    function isAbortSignal(val) {
        return typeof val === 'object' && ===;

    return new Promise(function (resolve, reject) {
        let timeoutHandle = null;

        function handleAbortEvent() {
            if (timeoutHandle !== null) {

            reject(new DOMException('Sleep aborted', 'AbortError'));

        if (signal !== null && isAbortSignal(signal)) {
            if (signal.aborted) {

            signal.addEventListener('abort', handleAbortEvent, {once: true});

        timeoutHandle = setTimeout(function () {
            if (signal !== null && isAbortSignal(signal)) {
                signal.removeEventListener('abort', handleAbortEvent);

        }, duration);

This function takes a delay in milliseconds as its first parameter, and an optional AbortSignal as its second parameter. It returns a Promise<void> which will resolve after the specified delay, or be rejected with an AbortError if cancellation is requested.

In the context of a React project, this can be used like the following within a useEffect hook:

useEffect(() => {
    const ac = new AbortController();

    async function fetchDataContinuously(abortController) {
        while (!abortController.signal.aborted) {
            try {
                await getData(abortController.signal);

                await asyncSleep(refreshInterval, abortController.signal);
            } catch (e) {
                if ( === 'AbortError') {

                console.error('Error continuously refreshing', e);


    return () => {
}, []);

Of course, this could also be used with a traditional class based React component by simply aborting the AbortController in componentWillUnmount as well.