DEV Community

Luka Vidaković
Luka Vidaković

Posted on

Timeout mechanism using Promise.race

So we have a scheduler factory function. Simple mechanism that runs code periodically. Here is the code:

function createScheduler({ callback, time }) {
  let run;
  let timeoutId;
  let ticker;

  function pause(time) {
    return new Promise(resolve => {
      timeoutId = setTimeout(resolve, time);
    });
  }

  async function* cycle(time) {
    run = true;

    while (run) {
      yield pause(time);
    }
  }

  async function runPeriodically() {
    if (!ticker || !run) {
      ticker = cycle(time);
    }

    for await (let tick of ticker) {
      await callback();
    }
  }

  function stop() {
    clearTimeout(timeoutId);
    run = false;
  }

  return {
    runPeriodically,
    stop
  };
}
Enter fullscreen mode Exit fullscreen mode

As said in the previous post, we can't blindly await on the callback function that is being passed from outside of our code. We can't know for sure how fast or even if it'll ever finish. To guard ourself from unexpected scenarios we'll implement a timeout mechanism using Promise.race function. It allows us to race with the response from the callback function. If callback isn't resolved until some amount of time we can force-skip into a next cycle. Code inside our "for await" loop will slightly change and incorporate our pause function which will act as a reference point in time before which we expect to see some results.

So instead of blindly awaiting on a callback:

await callback();
Enter fullscreen mode Exit fullscreen mode

This is how we would set a fixed 5 seconds timeout:

await Promise.race([callback(), pause(5000)]);
Enter fullscreen mode Exit fullscreen mode

This way we protect our piece of code from stalling when something goes bad within the logic of a callback function. To make it more ergonomic, let's parametrize the timeout duration and update our previous code:

function createScheduler({ callback, time, callbackTimeout }) {
  let run;
  let timeoutId;
  let ticker;

  function pause(time) {
    return new Promise(resolve => {
      timeoutId = setTimeout(resolve, time);
    });
  }

  async function* cycle(time) {
    run = true;

    while (run) {
      yield pause(time);
    }
  }

  async function runPeriodically() {
    if (!ticker || !run) {
      ticker = cycle(time);
    }

    for await (let tick of ticker) {
      if (callbackTimeout) {
        await Promise.race([callback(), pause(callbackTimeout)]);
      } else {
        await callback();
      }
    }
  }

  function stop() {
    clearTimeout(timeoutId);
    run = false;
  }

  return {
    runPeriodically,
    stop
  };
}
Enter fullscreen mode Exit fullscreen mode

It seems like we've covered potential pitfalls for regular use-cases, so next post will cover a bonus feature using async generators and will serve as a wrap for this series.

Top comments (0)