Skip to content

How can I do I cancel javascript await sleep?

The most common implementation of a sleep function in javascript is returning a Promise after setTimeout resolves:

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

I have for loop with await sleep to keep it from executing too fast, such as not requesting xhr too fast. I also have a isBreak flag elsewhere to tell me when to stop the for loop. However, the issue I have is that when I break the for loop, the previous await sleep has already executed and is holding up the for loop. Is there a better way of breaking the for loop and also terminating the await sleep instantaneously?

const items = [];

let isBreak = false; // Somewhere else in the application

for (const item of items) {
  if (isBreak) break;

  // Do something, like xhr request
  await sleep(15000); // 15 seconds sleep

  if (isBreak) break;
}

Is there a way for me to signal for early

Answer

In JS, when an await operation starts, it can no longer be interrupted; it will wait until its operand promise is settled.

So, you have to make the promise you’re awaiting cancelable in some way.

Unfortunately, your code can’t get notified about a variable reassignment (when you set isBreak to true), and polling it would be inefficient.

Instead of a flag, you could use an AbortSignal (which was invented for this purpose), and make your sleep accept one:

function sleep(ms, signal) {
  return new Promise((resolve, reject) => {
    const timeout = setTimeout(() => {
      resolve();
      signal.removeEventListener('abort', abort);
    }, ms);
    const abort = () => {
      clearTimeout(timeout);
      reject(new Error('Cancelled'));
    }
    if(signal.aborted)
      abort();
    else
      signal.addEventListener('abort', abort);
  });
}

Then, you use it like this:

const items = [];

const isBreak = new AbortController(); // Somewhere else in the application, call `isBreak.abort()`

try{
  for (const item of items) {
    // Do something, like xhr request
    await sleep(15000, isBreak.signal); // 15 seconds sleep
  }
}catch(e){
  if(e.message === 'Cancelled'){
    //Handle a cancellation
    console.log('Cancelled');
  }else{
    //Not a cancellation, rethrow it
    throw e;
  }
}

An AbortSignal also works well with fetch, if you have to cancel that too.