0

When using async await, sometimes we want to hold the promise in a variable, do something else, and then await it later. If the "something else" doesn't take very long this works fine:

try {
  const pWillReject = rejectAfter(8000);
  const someValue = await shortRunning();
  doSomethingWith(someValue);
  await pWillReject;
} catch (ex) {
  // handle the error
  console.log('caught', ex);
}
// continue

the error is caught by the catch as expected and code execution can continue

However, if the "something else" takes too long we may get the "Unhandled promise rejection" notice. Code execution is interuptted, the catch is not executed. This happens because, although we do eventually attach a handler, this isn't done until after the error event has already fired.

try {
  const pWillReject = rejectAfter(8000);
  const someValue = await longRunning();
  doSomethingWith(someValue);
  // we never get here
  await pWillReject;
} catch (ex) {
  //catch is not executed
  console.log('caught', ex);
}
// no oppurtunity to continue

I don't want to add a global handler for the UnhandledPromiseRejection since in most cases I want the program to terminate in this situation (since it represents a coding error). However in this case, where the rejection is handled (just not quickly enough) the program should continue.

What is a good workaround for this issue?

The best I can think of is to attach a do-nothing handler in a non-invasive way, i.e.

  const pWillReject = rejectAfter(8000);
  pWillReject.catch(ex=>{}); // do nothing, errors will be handled later

But I don't like this

  1. it makes the code noisy
  2. it isn't self documenting - a comment is required
  3. most importantly - it risks a genuine coding error being missed. If the promise is never awaited it could fail silently.

Is there a better option?

full code example:

async function go() {
  try {
    const pWillReject = rejectAfter(8000);
    const someValue = await shortRunning();
    doSomethingWith(someValue);
    await pWillReject;
  } catch (ex) {
    console.log('caught', ex);
  }
  console.log('success');
  try {
    const pWillReject = rejectAfter(8000);
    const someValue = await longRunning();
    doSomethingWith(someValue);
    await pWillReject;
  } catch (ex) {
    console.log('caught', ex);
  }
  console.log('failure');
}

go().catch(ex=> {
  console.log('global catch');
})

async function delay(ms) {
  return new Promise((r,x)=> {
    setTimeout(r, ms);
  });
}
async function rejectAfter(ms) {
  return new Promise((r,x)=> {
    setTimeout(x, ms, new Error("whatever"));
  });
}

function shortRunning() {
  return delay(1000);
}

function longRunning() {
  return delay(10000);
}

function doSomethingWith(obj) {
  console.info(obj);
}

N.B. In my case I am using node, however I think the question applies equally to browsers.

DJL
  • 2,060
  • 3
  • 20
  • 39
  • Your use-case is not entirely clear here. Why is there a need to call `rejectAfter` while it is being declared and await it only after another Promise has resolved? Even through this might work for "doing something else" for a short period of time, whether it takes long or not depends on factors such as network latency, meaning you can't guarantee it will finish either way. What is preventing you from calling the async function after the other function execution has finished like so `await rejectAfter(8000)`? – aside Mar 30 '21 at 08:53
  • because rejectAfter also takes some time. So we want to take advantage of parallelism here. – DJL Mar 30 '21 at 09:32
  • If you just want to make sure they run at the same time you could create an array containing the promises and pass it to `Promise.all()`, which you can then await. This will make them run in parallel and avoid a race condition. If you want to use `rejectAfter` as a sort of timeout function you could also use `Promise.any()` which will resolve once either `rejectAfter` or `longRunning` resolve. This would cause the trycatch-block to catch the rejection as well. – aside Mar 30 '21 at 09:41

0 Answers0