Implementing a cancellable asynchronous delay in JavaScript
Tags: javascript, promise, async
Published: 2022-07-19 18:09:02

---
title: Implementing a cancellable asynchronous delay in JavaScript
published: true
date: 2022-07-19 07:23:43 UTC
tags: js, promise, async
canonical_url: https://www.euantorano.co.uk/posts/2022/07/19/cancellable-async-delay-js/
---

While working on a React project recently, I had a need to update some state periodically with data from an API retrieved using [`fetch()`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch). Coming from a C# background, the way I would approach this problem there would be something like the following:

```C#
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()`](https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.delay?view=net-6.0).

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`](https://developer.mozilla.org/en-US/docs/Web/API/setTimeout) along with a [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/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`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) to cancel requests, I wanted to re-use that controller for cancellation.

Here's what I came up with:

```js
/**
 * 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' && val.constructor.name === AbortSignal.name;
    }

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

        function handleAbortEvent() {        
            if (timeoutHandle !== null) {
                clearTimeout(timeoutHandle);
            }

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

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

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

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

            resolve();
        }, 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:

```js
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 (e.name === 'AbortError') {
                    break;
                }

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

    fetchDataContinuously(ac).catch(console.error);

    return () => {
        ac.abort();
    };
}, []);
```

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