Search code examples
javascripterror-handlingpromiseasync-await

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!


Solution

  • 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.