DEV Community

Euan T
Euan T

Posted on • Originally published at euantorano.co.uk on

1

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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' && 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);
    });
}
Enter fullscreen mode Exit fullscreen mode

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

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

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

    return () => {
        ac.abort();
    };
}, []);
Enter fullscreen mode Exit fullscreen mode

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

Heroku

Build apps, not infrastructure.

Dealing with servers, hardware, and infrastructure can take up your valuable time. Discover the benefits of Heroku, the PaaS of choice for developers since 2007.

Visit Site

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay