20

Promisify a function call with timeouts

I have seen many resources provide similar examples of using Promise.race to timeout a function call within a given period of time. This is a very good example of how Promise.race can be used in practice. Here's some sample code:

function doWithinInterval(func, timeout) {
    var promiseTimeout = new Promise(function (fulfill, reject) {
       // Rejects as soon as the timeout kicks in
       setTimeout(reject, timeout);
    });
    var promiseFunc = new Promise(function (fulfill, reject) {
        var result = func(); // Function that may take long to finish
        // Fulfills when the given function finishes
        fulfill(result);
    });

    return Promise.race([promiseTimeout, promiseFunc]);
}

The simple approach above using Promise.race rejects the promise as soon as the timeout kicks in before func has completed. Otherwise, the project is fulfilled once the func function finishes before the timeout interval.

This sounds good and easy to use.

However, is this the best practice to use timeout in Promise?

Surely, the approach above can be employed if we want to set a timeout against a function call using Promises. The operations still appear to be a good promise. However, is this considered a good practice of using timeout in a Promise? If not, what is the disadvantage of using this?

I've look for alternative approaches, but couldn't find a native Promise way to do this.

Instead, some external Promise libraries offer timeout functionality as follows:

However, Promise.timeout() doesn't appear to be part of the standard ECMAScript 6 API (please correct me if I'm wrong). Is there any recommended way to handle timeouts natively with ES6 Promises?

Dan Dascalescu
  • 143,271
  • 52
  • 317
  • 404
TaoPR
  • 5,932
  • 3
  • 25
  • 35
  • 3
    Given that ES6 Promises are not cancellable, there's not much else you could do than `race` the task against a delayed rejection. – Bergi Jun 19 '15 at 11:28
  • I have also explored through the available official ES6 Promise functionality and can't find other comparable way too. – TaoPR Jun 19 '15 at 11:53

2 Answers2

18

It depends on what you mean by timeout.

If you expect the function to stop, then no.

If you just want to stop waiting for it, then yes (quick to whip up in ES6):

var wait = ms => new Promise(resolve => setTimeout(resolve, ms));
var timeout = (p, ms) => Promise.race([p, wait(ms).then(() => {
    throw new Error("Timeout after " + ms + " ms");
})]);

var wait = ms => new Promise(resolve => setTimeout(resolve, ms));
var timeout = (p, ms) => Promise.race([p, wait(ms).then(() => {
  throw new Error("Timeout after " + ms + " ms");
})]);

// Example:

var log = msg => div.innerHTML += "<p>" + msg + "</p>";
var failed = e => log(e.toString() + ", line " + e.lineNumber);

log("Waiting 5 seconds...");
timeout(wait(5000), 2000)
.then(() => log("...Done."))
.catch(failed);
<div id="div"></div>

If you want to cancel the operation (make it stop), then hopefully that operation comes with an API to cancel it, and you should use that, since an ES6 promise is not a control surface.

Cancelable promises is a controversial topic in ES6, but some of the libraries mentioned do offer the concept.

jib
  • 40,579
  • 17
  • 100
  • 158
  • By the term "timeout", it would infer that the output of the function which fails to produce output within a period of time will no longer matter. So either case may make no difference in terms of usage because regardless the function is allowed to run after timeout, the output is not taken into account. (even though both are totally different). – TaoPR Jun 19 '15 at 16:01
  • 2
    @TaoP.R. - This assumes a function with no side-effects. Say the function plays music and resolves the promise once the track is complete. If we launch a second track after a 20 second timeout, then it matters a lot whether the first track plays for only 20 seconds or whether the two tracks play on top of each other. At best, a running function consumes CPU, so I think it's always better to be clear about the desired effect and not treat *canceling* and *abandoning* the same. – jib Jun 19 '15 at 23:50
  • Very good point indeed. However, for general cases the timeout may just trigger a "rejection" signal without potentially killing the ongoing process. If the developer want to, it should be manually taken to prevent undesired behavior. So let's consider only the case where the function still carry on asynchronously but the Promise no longer waits for it. – TaoPR Jun 20 '15 at 05:40
16

The native Promise.race method doesn't clear the timer of the timeout promise after the actual promise completes thus the process will wait until the timeout promise is also complete. This means that if you set the timeout to 1h and our promise is completed after 1min then the process will wait for 59min before it exits.

Use this method instead:

export function race({promise, timeout, error}) {
  let timer = null;

  return Promise.race([
    new Promise((resolve, reject) => {
      timer = setTimeout(reject, timeout, error);
      return timer;
    }),
    promise.then((value) => {
      clearTimeout(timer);
      return value;
    })
  ]);
}
xpepermint
  • 35,055
  • 30
  • 109
  • 163