Search code examples
javascriptes6-promiseanti-patternsnon-recursive

Is async code in a Promise always an antipattern?


I see from this question that it can be an antipattern to mix Promises with async code.

Does this, however, apply in all cases?

I can't see an easy way to avoid combining them in the following code:

  • It's an alternative to setInterval which waits for an invocation to complete before scheduling the next one
  • However, unlike recursive setTimeout, it does not constitute an open memory leak in browsers that don't yet support TCO

Does this code embody an antipattern? And, if so, how can I remedy it without introducing a memory leak?

See, in particular, line 10: new Promise( async (resolve) => {

—this seems very non-idiomatic, but I don't see another way to accomplish: wrapping an await statement in a while loop per se, dispatching it, and returning a handle to abort the loop.

var [setRepeatedTimeout, clearRepeatedTimeout] = (() => {
    const asleep = (delay) => new Promise(resolve => setTimeout(resolve, delay));
    const repeatedTimeoutIntervals = [];

    function setRepeatedTimeout(f, delay, ...arguments) {
        //Like setInterval, but waits for an invocation to complete before scheduling the next one
        //(Supports both classic and async functions)
        const mySemaphores = {notAborted: true};
        const intervalID = repeatedTimeoutIntervals.push(mySemaphores) - 1;
        new Promise( async (resolve) => {
            await asleep(delay);
            while(mySemaphores.notAborted) {
                await f(...arguments);
                await asleep(delay);
            }
            delete repeatedTimeoutIntervals[intervalID];
        });
        return intervalID;
    }

    function clearRepeatedTimeout(intervalID) {
        //Clears loops set by setInterval()
        repeatedTimeoutIntervals[intervalID].notAborted = false;
    }

    return [setRepeatedTimeout, clearRepeatedTimeout];
})();
<p><button onclick="(function createInterval(){
  const _ = {intervalID: undefined};
  _.intervalID = setRepeatedTimeout( () => {
    console.log(`Hello from intervalID ${_.intervalID}`)
  }, 2000)
})()">Create timer</button><br />
<form action="javascript:void(0);" onsubmit="(function clearInterval(intervalID){
  clearRepeatedTimeout(intervalID);
})(parseInt(event.target.elements.intervalID.value))">
<input name="intervalID" placeholder="intervalID"/><button type="submit">Clear timer</button></p>


Solution

  • The problem that the other question was warning about, and that could be a problem here, is that if the inside of the async callback passed to the Promise constructor awaits something that rejects, the Promise will hang instead of rejecting. Your current code will not result in f ever rejecting, but setRepeatedTimeout were to carry out a task which may reject, you'd get an unhandled rejection and permanent hanging:

    var [setRepeatedTimeout, clearRepeatedTimeout] = (() => {
        const asleep = (delay) => new Promise(resolve => setTimeout(resolve, delay));
        const repeatedTimeoutIntervals = [];
    
        function setRepeatedTimeout(f, delay, ...arguments) {
            //Like setInterval, but waits for an invocation to complete before scheduling the next one
            //(Supports both classic and async functions)
            const mySemaphores = {notAborted: true};
            const intervalID = repeatedTimeoutIntervals.push(mySemaphores) - 1;
            new Promise( async (resolve) => {
                await asleep(delay);
                while(mySemaphores.notAborted) {
                    await f(...arguments);
                    await asleep(delay);
                }
                delete repeatedTimeoutIntervals[intervalID];
            });
            return intervalID;
        }
    
        function clearRepeatedTimeout(intervalID) {
            //Clears loops set by setInterval()
            repeatedTimeoutIntervals[intervalID].notAborted = false;
        }
    
        return [setRepeatedTimeout, clearRepeatedTimeout];
    })();
    
    
    const _ = { intervalID: undefined };
    _.intervalID = setRepeatedTimeout(() => {
      console.log('Throwing...');
      return Promise.reject();
    }, 2000)

    If you want the loop to continue when such an error is encountered, there's a way to handle such problems while keeping the async: just catch everything that might reject (either in a try/catch or with .catch):

    var [setRepeatedTimeout, clearRepeatedTimeout] = (() => {
        const asleep = (delay) => new Promise(resolve => setTimeout(resolve, delay));
        const repeatedTimeoutIntervals = [];
    
        function setRepeatedTimeout(f, delay, ...arguments) {
            //Like setInterval, but waits for an invocation to complete before scheduling the next one
            //(Supports both classic and async functions)
            const mySemaphores = {notAborted: true};
            const intervalID = repeatedTimeoutIntervals.push(mySemaphores) - 1;
            new Promise( async (resolve) => {
                await asleep(delay);
                while(mySemaphores.notAborted) {
                    await f(...arguments).catch(() => {}); // log error here if you want
                    await asleep(delay);
                }
                delete repeatedTimeoutIntervals[intervalID];
            });
            return intervalID;
        }
    
        function clearRepeatedTimeout(intervalID) {
            //Clears loops set by setInterval()
            repeatedTimeoutIntervals[intervalID].notAborted = false;
        }
    
        return [setRepeatedTimeout, clearRepeatedTimeout];
    })();
    
    
    const _ = { intervalID: undefined };
    _.intervalID = setRepeatedTimeout(() => {
      console.log('Throwing...');
      return Promise.reject();
    }, 2000)

    But there's really no need for the new Promise here at all - it never resolves, and never gets used. Just use an async IIFE:

    var [setRepeatedTimeout, clearRepeatedTimeout] = (() => {
        const asleep = (delay) => new Promise(resolve => setTimeout(resolve, delay));
        const repeatedTimeoutIntervals = [];
    
        function setRepeatedTimeout(f, delay, ...arguments) {
            //Like setInterval, but waits for an invocation to complete before scheduling the next one
            //(Supports both classic and async functions)
            const mySemaphores = {notAborted: true};
            const intervalID = repeatedTimeoutIntervals.push(mySemaphores) - 1;
            (async () => {
                await asleep(delay);
                while(mySemaphores.notAborted) {
                    await f(...arguments).catch(() => {}); // log error here if you want
                    await asleep(delay);
                }
                delete repeatedTimeoutIntervals[intervalID];
            })();
            return intervalID;
        }
    
        function clearRepeatedTimeout(intervalID) {
            //Clears loops set by setInterval()
            repeatedTimeoutIntervals[intervalID].notAborted = false;
        }
    
        return [setRepeatedTimeout, clearRepeatedTimeout];
    })();
    
    
    const _ = { intervalID: undefined };
    _.intervalID = setRepeatedTimeout(() => {
      console.log('Throwing...');
      return Promise.reject();
    }, 2000)

        (async () => {
            await asleep(delay);
            while(mySemaphores.notAborted) {
                await f(...arguments).catch(() => {}); // log error here if you want
                await asleep(delay);
            }
            delete repeatedTimeoutIntervals[intervalID];
        })();