Search code examples
node.jspromiseunhandled

Nodejs duplicate rejection (unhandled one and in a try/catch block)


By using node js 12.16.1 LTS I don't understand why this piece of code leads to a double rejection (one unhandled and one catched). When I remove the p promise and await p in create_bug(), it works well (Only one rejection catched in a try catch block). I cannot figure out why.

Nodejs experts, could you please help ?

'use strict';

process.on('uncaughtException', (err) => {
  console.error(`uncaughtException: ${JSON.stringify({name: err.name, msg: err.message})}`);
});
process.on('unhandledRejection', (err) => {
  console.error(`unhandledRejection: ${JSON.stringify({name: err.name, msg: err.message})}`);
});

async function create_bug() {
  console.log('In create');
  let res = superCreate();
  console.log(`In create, res = ${res}`);
  let p = new Promise((a, r) => setTimeout(() => a(), 0));
  await p;
  return res;
}


async function superCreate() {
  console.log('superCreate : now throwing');
  throw new Error("Something wrong");
}

async function create_OK() {
  console.log('In create');
  let res = await superCreate();
  console.log(`In create, res = ${res}`);
  let p = new Promise((a, r) => setTimeout(() => a(), 0));
  await p;
  return res;
}

async function main() {
  try {
    let res = await create_bug();
    console.log(`create result : ${res}`);
  } catch (err) {
    console.error(`ERROR caught in main : ${JSON.stringify({name: err.name, msg: err.message})}`);
  }
}

main().then(() => {
  setTimeout(() => console.log(`Finished`), 2000);
});

Solution

  • The promise contained in the variable res from your superCreate is not awaited and there is not attached a catch handler to it before it gets rejected. Therefore the unhandled promise rejection is triggered. A handler is attached after the rejection when the await is triggered in main.

    Note that a rejection handler is invoked even though it is attached on a promise after it is rejected. Try e.g.:

    async function main() {
      let res = create_bug();
      try {
        await res;
        console.log(`create result : ${res}`);
      } catch (err) {
        console.error(`ERROR caught in main : ${JSON.stringify({name: err.name, msg: err.message})}`);
      }
      res.catch(err => console.error(`main1: ${JSON.stringify({name: err.name, msg: err.message})}`));
      res.catch(err => console.error(`main2: ${JSON.stringify({name: err.name, msg: err.message})}`));
    }
    

    Notice that you will now also get the "main1" and "main2" errors.

    Alternatively, try removing the async from the superCreate function, now you should see that the In create, res = ${res} is not printed, but instead the exception is handled synchronously.

    Yet another alternative is to simply return res directly from create_bug without any await and instead await the res in main. Then you will see similar behavior to your original: both an unhandled rejection and the "normal" catch-handling block.

    async function create_bug() {
      console.log('In create');
      let res = superCreate();
      console.log(`In create, res = ${res}`);
      return res;
    }
    
    async function superCreate() {
      console.log('superCreate : now throwing');
      throw new Error("Something wrong");
    }
    
    async function main() {
      try {
        let res = create_bug();
        let p = new Promise((a, r) => setTimeout(() => a(), 0));
        await p;
        await res;
        console.log(`create result : ${res}`);
      } catch (err) {
        console.error(`ERROR caught in main : ${JSON.stringify({name: err.name, msg: err.message})}`);
      }
    }