14

I'm trying to wrap my head around this issue I'm facing concerning async/await and Promises. I managed to boil my issue down to the following code:

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

async function fetchMock(): Promise<any> {
  return new Promise(() => {
    throw 'error fetching result';
  });
}

async function main(): Promise<any> {
  const kickedOffRequest = fetchMock();
  await sleep(10);
  return kickedOffRequest;
}

main()
  .then(() => console.log('resolved promise!'))
  .catch(error => console.error('caught error!', error));

I receive the following warning:

(node:82245) UnhandledPromiseRejectionWarning: error fetching result
(node:82245) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). (rejection id: 1)
(node:82245) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
caught error! error fetching result
(node:82245) PromiseRejectionHandledWarning: Promise rejection was handled asynchronously (rejection id: 1)

You can observe the same issue in this sandbox. I noticed that commenting out the await sleep(10) fixes the issue, but I apparently know less about promises than I thought. Why does commenting that line out make my program work? I'm tempted to ask how to fix the Promise rejection was handled asynchronously error, but I hope that once I understand how await sleep(10) causes the error I get I will be able to fix this one on my own.

Thanks in advance for taking the time to read/answer this question!

marhaupe
  • 267
  • 1
  • 3
  • 10
  • 1
    @Bergi - I don't see how the duplicate you marked actually provides a solution to this specific problem. What the OP appears to want in `main()` is to return the promise from `fetchMock()` whether rejected or resolved, but not have it resolve or reject until at least 10ms have passed. I don't see how to do that in your dup. So, if `fetchMock()` resolves or reject immediately, then the promise returned from `main()` wouldn't resolve or reject for 10ms. But, if the promise from `fetchMock()` took more than 10ms to resolve or reject, no further delay would be added to that resolve or reject. – jfriend00 Nov 26 '19 at 23:26
  • @jfriend00 to me, it was unclear what behaviour the OP really wants, but the dupe is our canonical explaining why the current attempt doesn't work and why `Promise.all` should be used instead – Bergi Nov 26 '19 at 23:41
  • 2
    Well, there's no obvious use of `Promise.all()` that creates the exact timing that `main()` shows. [Canonical answers](https://stackoverflow.com/questions/46889290/waiting-for-more-than-one-concurrent-await-operation) work fine for canonical questions, not questions that raise a different variant for which a solution is not shown in the canonical answer. – jfriend00 Nov 26 '19 at 23:58
  • @jfriend00 That's exactly what I want, thank you. In my "real" program I kick off a network request and then sleep for some time. I don't want to await my network request at this point, so `Promise.all` is not the thing I want here. – marhaupe Nov 27 '19 at 09:15
  • Do you want an answer that actually does what you described - including rejecting the returned promise rather than the work-around in the answer you've already accepted? I figured out a way to do it, but it will take me a bit of time to make sure it works properly. – jfriend00 Nov 27 '19 at 09:54
  • Sure, I would be very thankful! I honestly was not aware that I accepted a workaround, I just thought that that was the way to go to prevent the `Unhandled promise rejection`. If you found a way to reject the promise I'd be glad to see your solution! – marhaupe Nov 27 '19 at 10:00

3 Answers3

14

The original concept of promises was that you could have a rejected promise sitting around for some time before attaching a catch handler to it. For example, Firefox used to warn of uncaught rejection errors only when a rejected promise with no rejection handler was garbage collected from memory.

Somebody decided that programmers couldn't be trusted with managing promise rejections properly and changed the HTML spec to require browsers to throw "unhandled promise rejection" errors if a rejected promise has no rejection handlers added before code returns to the event loop.

(I think unhandled rejections can survive without error in the micro task queue for a tick or two, before control returns to the event loop proper, but haven't tested it lately.)

The ECMAScript specification added an abstract means of notifying the host environment of an unhandled promise rejection without specifying what, if any, action should be taken.

On a case by case basis you can prevent the host being notified by adding a rejection handler that is never used. The reasoning is that adding a dummy rejection handler to a promise means that should it be rejected it has a rejection handler already - or if it was rejected the host is notified the promise now has a rejection handler - and you can call then and catch multiple times on the same promise.

Changing

async function fetchMock(){
  return new Promise(() => {
    throw 'error fetching result';
  });
}

to

async function fetchMock(){
  let promise = new Promise(() => {
    throw 'error fetching result';
  });
  promise.catch(()=>null); // unused rejection handler
  return promise;
}

should work around the unwanted HTML5 host behavior implemented in V8, the JavaScript engine used in node.

traktor
  • 17,588
  • 4
  • 32
  • 53
6

The detection of unhandled rejection in node.js is imperfect. There are specific spots in the life cycle of a rejected promise where the engine checks to see if there's a handler and it does not always wait until the last possible moment so it can miss places that you add a handler. In your specific case, you may need to attach a .catch() handler locally, then finish up the work you want to do, then rethrow the error. This work-around will work for you while still maintaining the desired resolve/reject from main() (e.g. without changing the interface to main).

So, this isn't particularly super pretty, but it meets the spec we talked about in comments.

  1. main() calls fetchMock()
  2. If it resolves or rejects quickly (before some custom delay time), then it holds off on the resolve or the reject until at least that delay time has elapsed from when fetchMock() was originally called.
  3. If fetchMock() takes longer than that custom delay time to resolve or reject, then no further delay is added.
  4. The promise that main() returns then follows the promise that fetchMock() returned, either rejected or resolved with the same reason or value.

The key ingredient is that it captures the time right before calling fetchMock() and then when fetchMock() either resolves or rejects, it decides whether to delay any more time before passing the resolve/reject value/reason on through.

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

function fetchMock() {
    return new Promise((resolve) => {
        throw 'error fetching result';
        //resolve('this is our result');
    });
}


function handler(start, minWaitTime, isErr = false) {
    return async function(val) {
        let diff = minWaitTime - (Date.now() - start);
        if (diff > 0) {
            await sleep(diff);
        }
        if (isErr) {
            throw val;
        } else {
            return val;
        }
    }
}

function main() {
    let start = Date.now();
    const minWaitTime = 1000;
    return fetchMock().then(handler(start, minWaitTime), handler(start, minWaitTime, true));
}

main()
    .then(() => console.log('resolved promise!'))
    .catch(error => console.error('caught error!', error));

Note, also that sleep() and fetchMock() already directly return promises and don't use await so there is no requirement for them to be async.

jfriend00
  • 683,504
  • 96
  • 985
  • 979
  • @marcelHaupenthal - OK, here's a way to do it. – jfriend00 Nov 27 '19 at 10:24
  • You're awesome! Thanks for taking your time for me. I think this is the solution I was looking for. I'm going to have to read some more documentation about promises, but this helps me alot! Have a great day. – marhaupe Nov 27 '19 at 13:54
1

The problem is that the fetchMock rejects immediately, and when a Promise rejects, at the time that it rejects, it must be chained with a .catch somewhere in order to prevent an Unhandled Promise Rejection.

With your await sleep(10), the kickedOffRequest promise rejects while the main function is still waiting for sleep to resolve. When there's a rejection, the interpreter doesn't look ahead to see if the Promise may be caught in the future (for example, to see if the Promise gets returned or caught) - the Promise must be caught now.

When you remove the await, kickedOffRequest still becomes a rejected Promise, but it's returned from main immediately, so at the point that the Promise rejects, it can be seen and caught by the outer .catch, so there's no Unhandled Rejection warning.


It's good if a Promise rejection can be handled right when it rejects, but if that's not an option for you, you can put another .catch onto the end of the inner Promise. That way, the warning won't appear (because of the existence of .catch, even if that .catch didn't do anything meaningful) and you can later check to see if an error actually occurred or not:

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

async function fetchMock(){
  return new Promise(() => {
    throw 'error fetching result';
  });
}

async function main() {
  const kickedOffRequest = fetchMock()
    .then(resolved => ({ resolved }))
    .catch(rejected => ({ rejected }));
  await sleep(10);
  return kickedOffRequest;
}

main()
  .then(({ resolved, rejected }) => {
    if (resolved) {
      console.log('resolved promise!');
    } else {
      console.error('caught error!', rejected);
    }
  });

This is pretty similar to how Promise.allSettled works (though it's more intended for when there's an array of Promises that need to be parsed):

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

async function fetchMock(){
  return new Promise(() => {
    throw 'error fetching result';
  });
}

async function main() {
  const kickedOffRequestArr = Promise.allSettled([fetchMock()]);
  await sleep(10);
  return kickedOffRequestArr;
}

main()
  .then(([{ result, reason, value }]) => {
    if (result === 'fulfilled') {
      console.log('resolved promise!', value);
    } else {
      console.error('caught error!', reason);
    }
  });
CertainPerformance
  • 356,069
  • 52
  • 309
  • 320
  • Thank you! That clears things up. I'm not sure how I feel about returning a Promise that resolves to a "wrapper" around an error or an actual response. I liked just throwing the error in a nested function and making the user of `main` react to the error, but if I understood correctly, that's not really possible, is it? – marhaupe Nov 27 '19 at 09:23
  • 1
    Right. It's possible, but only barely - it's not a good idea due to the rejection warning, which will eventually become an error by Node (at which point it will be actually impossible). – CertainPerformance Nov 27 '19 at 09:28