JavaScript Promise: Resolving Unhandled Promise Rejection without try...catch Block

JavaScript Promise: Resolving Unhandled Promise Rejection without try...catch Block

If you're aware, JavaScript Promise has an implicit rejection even without the .catch() block. But if you don't explicitly handle the error, the Firebase Cloud Functions will treat it as Unhandled Promise Rejection. Unfortunately, even if you handle the error with Promise.catch() as shown below, you'll not be able to get the caller notified of the error:


function test() {
  return new Promise((resolve, reject) => {
      throw new Error("oops!");
    })
    .catch(error => {
      console.log(error);
    });
}

Note that the reject callback parameter is not available in the .catch() block. So with the above example, the caller will take it as NO ERROR since the error has already been handled:


test()
  .then(() => {
    // With Promise.catch(), this will be triggered instead.
    alert("OK");
  })
  .catch(error => {
    alert(error.message);
  });

In order for the caller to detect the error, for Promise.catch() block, you'll have to do this:


function test() {
  return new Promise((resolve, reject) => {
      throw new Error("oops!");
    })
    .catch(error => {
      console.log(error);
      // This line need to be added for the caller to detect the error.
      return Promise.reject(error);
    });
}

Now your caller can detect the error under the catch() block. But if you have a nested call in the then() block, be sure to use the return statement for every Promise function call, or else the Unhandled Promise Rejection will still be triggered by Node.js:


test()
  .then(() => {
    alert("OK");
    // Use the return statement for a single last catch() to work.
    return test()
    	.then(() => {
           alert("OK");
        })
  })
  // With the return statement, only one last catch is suffice.
  .catch(error => {
    alert(error.message);
  });

With this approach, you can have a cleaner Promise code block without the try...catch as shown below:


function test() {
  return new Promise((resolve, reject) => {
    try {
      throw new Error("oops!");
    } catch (error) {
      return reject(error);
    }

  });
}

Now you may ask: Then when to use the reject callback? -- My answer is: Business Logic Errors. Let the Promise.catch() handles the unexpected exception and use the reject callback to handle the business logic errors:


function test(x) {
  return new Promise((resolve, reject) => {
      if (x < 10) {
      	return reject(new Error("[Business Error]: x must be greater than or equal to 10!"));
      };
      resolve(true);
    })
    .catch(error => {
      console.log(error);
      return Promise.reject(error);
    });
}

This makes sense because it could allow the caller to identify which error is business logic error and which is not and handle the error accordingly. For example, you might have some custom error objects for your business errors with predefined error codes. Of course, you can simply throw a new error or custom error object without the reject callback, the .catch() block in the above example should pick up the error and notify the caller accordingly.

Conclusion:

So the key highlight in this article is: Return Promise.reject() in the .catch() block even without the try...catch for the caller to be notified with the error. This will also help resolving the Unhandled Promise Rejection warned by Firebase Cloud Functions.