DEV Community

dbigpot
dbigpot

Posted on • Updated on

 

JavaScript: Error handling with Promises and Async/Await

Error handling in JavaScript can be easy while being tricky at certain places, especially Promises. JS allows error handling with the help of try, catch, and throw.

const main = () => {
  try {
    // Do something super crazy
    if (!bakePizza()) {
      throw new Error('Oh no!');
    }
  } catch (error) {
    // That escalated quickly
    handleOvenExplosion();
  }
}

This seems simple enough but gets a little tricky when Promises are involved.

Let’s look at a simple example of a Promise. The following Promise function fetches a list of user profiles from the database, where the result set is resolved by the promise function and the error is rejected.

const userProfileQuery = new Promise((resolve, reject) => {
  connection.query('SELECT * FROM Users', [], (err, result) => {
    if (err) reject({ type: 'SQL', err});
    connection.release();
    resolve(result);
});
userProfileQuery
  .then((data) => {
    const userList = data;
    // Do something crazy with the list
  })
  .catch((err) => {
    // Oh, snap!
    // Handle error
  });

In an ideal world, we’d want to have a single try-catch block to handle all errors that occur in that single file.

const { getUserProfiles } = require('./helpers');
module.exports = () => {
  try {
    let userProfileList;
    getUserProfiles
      .then((data) => {
        userProfileList = data;
      })
      .catch((error) => {
        // Handle Promise Error
        // All errors thrown in this promise land here
      });
  } catch (error) {
    // Handle errors in this module
  }
}

The above module is simple — It fetches a list of user profiles with the help of a Promise function.

But the problem with the above module is that when we throw a new Error inside the then block of the promise, it will always pass to the catch block of the promise. That is because throwing a new error inside a then block of a promise will always be passed to the catch block of the invoking promise function. This does not allow us to handle all errors in a module with a singular try-catch block.

But, alas! There is a way to handle this with the help of Async/Await. Let me explain this better with an example —

const { getUserProfiles } = require('./helpers');
module.exports = async () => {
  try {
    const userProfileList = await getUserProfiles;
  } catch (error) {
    // Handle errors in this module
    switch (type) {
      case ERROR_SQL:
        // Handle SQL errors
      default:
        // Handle common errors
    }
  }
}

This little addition of async/await in your code does two things —
Assign the value to the variable which was resolved by the promise function.

Throw error if the promise function rejects anything.
Note that the value assignment works only when a promise functions resolves some value and errors get thrown only when the promise function rejects something.

This way, async/await lets us keep our code clean, maintainable, and easy to read.


Thanks for reading. If you have thoughts on this, be sure to leave a comment.

Top comments (5)

Collapse
 
willemodendaal profile image
Willem Odendaal

Very useful, thank you :) I was just a bit confused by this statement, it seems to contradict itself?:

"But the problem with the above module is that when we throw a new Error inside the then block of the promise, it will always pass to the catch block of the promise. That is because throwing a new error inside a then block of a promise will always be passed to the catch block of the invoking promise function."

Collapse
 
fluffystark profile image
John Matt

I think the catch block it was referring to is the catch block right after the then block. But the goal was to have a single catch block do all the error handling but from that code, it had two catch blocks ( 1. for module errors, 2. for promise/then errors )

Collapse
 
dbigpot profile image
dbigpot • Edited

@fluffystark pretty much answers your question. However, I'll explain it further just in case -

Say you have a promise function called somePromise. When you do a throw new Error() inside your .then() block, like below

try {
    somePromise()
        .then((data) => {
            throw new Error('Inner error');
        })
        .catch((promiseError) => {
            console.log(promiseError); // Prints Inner error
        });
} catch (error) {
    // Handle all module errors here
}

the error will get passed to the .catch() block of somePromise() and not the catch block of the try-catch.

When having multiple promise statements in your code, it can become cumbersome to handle each promise's errors in its own .catch() block. Using async/await allows you to handle all errors in one place. I hope this cleared your doubt. :)

Collapse
 
toddhgardner profile image
Todd H. Gardner

Thanks for writing this! Using promises as a wrapper around logic that could fail is a useful pattern. You can also listen for promise errors globally (rather than attaching a catch to each one) using the unhandledrejection global event.

It’s also important to log these sort of failures back to a service like TrackJS to help you understand when things fail in production.

Collapse
 
azarouamine profile image
AzarouAmine

Somewhat off-topic, async/await were introduced in ES8 not ES6.

Visualizing Promises and Async/Await 🤯

async await

☝️ Check out this all-time classic DEV post