Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to handle an unhandled promise rejection asynchronously?

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!

like image 399
marhaupe Avatar asked Nov 26 '19 22:11

marhaupe


People also ask

How do you handle promises in asynchronous function?

Inside an async function, you can use the await keyword before a call to a function that returns a promise. This makes the code wait at that point until the promise is settled, at which point the fulfilled value of the promise is treated as a return value, or the rejected value is thrown.

How should you handle rejected promises when using async await?

In async functions, there is no resolve or reject function. There's return and throw , which are the idiomatic ways to resolve and reject the async function's promise.

How do you handle rejection promises?

We must always add a catch() , otherwise promises will silently fail. In this case, if thePromise is rejected, the execution jumps directly to the catch() method. You can add the catch() method in the middle of two then() methods, but you will not be able to break the chain when something bad happens.

Do promises run asynchronously?

A promise is used to handle the asynchronous result of an operation. JavaScript is designed to not wait for an asynchronous block of code to completely execute before other synchronous parts of the code can run. With Promises, we can defer the execution of a code block until an async request is completed.


2 Answers

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.

like image 58
traktor Avatar answered Nov 15 '22 17:11

traktor


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.

like image 29
jfriend00 Avatar answered Nov 15 '22 18:11

jfriend00