Scheintod
Scheintod

Reputation: 8105

Break out of for(ever) loop by resolved Promise

I hope this is a simple question but I currently can't wrap my head around.

What I want to do is break out of a while loop which contains a delay when a promise gets resolved.

In pseudocode this would look like:

while( ! promise.resolved ){
    doSomthing()
    await sleep( 5min )
}

The loop must break instantly after the promise is resolved and not wait for sleep to finish.

sleep is currently implemented trivially by setTimeout but can be implemented differently.

I would like to have some kind of spatial separation between the awaited promise and sleep to show its working more clearly*) (and because I hope for an elegant solution to learn from). So what would work but I don't like is something like:

while( true ){

    doSomething()
    try {
        await Promise.race([promise,rejectAfter(5000)])
        break
    } catch( e ){}
} 

If you must know:

doSomething is sending out status information.

promise is waiting for user interaction.

*) Part of the purpose of this code is to show/demonstrate others how things are expect to work. So I'm looking for the clearest solution on this level of implementation.

Upvotes: 3

Views: 1700

Answers (4)

Bergi
Bergi

Reputation: 664494

One approach would be to wait for the promise result, assuming it is a truthy value1:

const promise = someTask();
let result = undefined;
while (!result) {
    doSomething();
    result = await Promise.race([
        promise, // resolves to an object
        sleep(5000), // resolves to undefined
    ]);
}

1: and if it isn't, either chain .then(_ => true) to the promise, or make sleep fulfill with a special value that you can distinguish from everything someTask might return (like the symbol in Jeff's answer).

Also works nicely with a do-while loop, given result is always undefined at first:

const promise = someTask();
let result = undefined;
do {
    doSomething();
    result = await Promise.race([
        promise, // resolves to an object
        sleep(5000), // resolves to undefined
    ]);
} while (!result);

A downside here is that if doSomething() throws an exception, the promise is never awaited and might cause an unhandled rejection crash when the task errors.


Another approach would be not to use a loop, but an old-school interval:

const i = setInterval(doSomething, 5000);
let result;
try {
    result = await someTask();
} finally {
    clearInterval(i);
}

A downside here is that doSomething is not called immediately but only after 5s for the first time. Also, if doSomething throws an exception, it will instantly crash the application. It still might be a good approach if you don't expect doSomething to throw (or handle each exception in the setInterval callback and expect the "loop" to carry on).


The "proper" approach that will forward all exceptions from both someTask() and doSomething() could look like this:

let done = false;
const result = await Promise.race([
    (async() => {
        while (!done) {
            doSomething();
            await sleep(5000)
        }
    })(),
    someTask().finally(() => {
        done = true;
    }),
]);

(Instead of .finally(), you can also wrap the Promise.race in a try-finally, like in approach two.)

The only little disadvantage in comparison to approach two is that sleep(5000) will keep running and is not immediately cancelled when someTask finishes (even though result is immediately available), which might prevent your program from exiting as soon as you want.

Upvotes: 2

VLAZ
VLAZ

Reputation: 29007

Minor modification of your idea to make it work better:

const sleep = ms =>
    new Promise(resolve => setTimeout(resolve, ms));

async function waitAndDo(promise) {
  let resolved = false;
  const waitFor = promise
    .then((result) => resolved = true);
    
  while(!resolved) {
    doSomething();
    await Promise.race([waitFor, sleep(5000)]);
  }
}
  1. The function accepts a promise and will be working until it resolves.
  2. The waitFor promise will finish after promise is fulfilled and resolved updated to true.
  3. The while loop can then loop until the resolved variable is set to true. In that case, the loop will end an execution continues after it.
  4. Inside the loop, Promise.race() will ensure that it will stop awaiting as soon as the promise resolves or the sleep expires. Whichever comes first.

Therefore, as soon as the promise gets resolve, the .then() handler triggers first and updates resolved. The await Promise.race(); will end the waiting after and the while loop will not execute again, since resolved is now true.

Upvotes: 2

Jeff Bowman
Jeff Bowman

Reputation: 95634

As an alternative to VLAZ's very reasonable answer, you can avoid the separate boolean sentinel by having your sleep function return some kind of unique sentinel return value that indicates the the timeout. Symbol is exactly the kind of lightweight, unique object for this use case.

function sleepPromise(ms, resolveWith) {
  return new Promise(resolve => {
    setTimeout(resolve, ms, resolveWith);
  });
}

const inputPromise = new Promise(
    resolve => document.getElementById("wakeUp").addEventListener("click", resolve));

async function yourFunction() {
  const keepSleeping = Symbol("keep sleeping");
  do {
    /* loop starts here */
    console.log("Sleeping...");
    /* loop ends here */
  } while (await Promise.race([inputPromise, sleepPromise(3000, keepSleeping)]) === keepSleeping);
  console.log("Awake!");
}

yourFunction();
<button id="wakeUp">Wake up</button>

Upvotes: 1

epascarello
epascarello

Reputation: 207501

Seems like you would just use some sort of interval and kill it when the promise is done.

const updateMessage = (fnc, ms, runInit) => {
  if (runInit) fnc();
  const timer = window.setInterval(fnc, ms);
  return function () {
    console.log('killed');
    timer && window.clearTimeout(timer);
  }
}

const updateTime = () => {
  document.getElementById("out").textContent = Date.now();
}

const updateEnd = updateMessage(updateTime, 100, true);

new Promise((resolve) => {
  window.setTimeout(resolve, Math.floor(Math.random()*5000));
}).then(updateEnd);
<div id="out"></div>

Upvotes: 1

Related Questions