Preview:
Handling Errors in JS
JavaScript doesn't have a universal way of handling errors, which means we're sometimes faced with a pretty difficult task. Error handling is implemented differently depending on whether the functions are synchronous or asynchronous, and the approach to implementing asynchrony can vary as well. Let's examine these cases.

Error handling in synchronous code
To handle errors in synchronous code, we use the try...catch statement.
You're already familiar with it, so let's briefly recall how it works. We place the error handling code in the try block, and describe how to handle the error in the catch block. 

For simple synchronous code, that's that. Unfortunately, the try...catch statement has a significant drawback: since it doesn't work with asynchronous code, it'll skip any errors that occur in promises or callbacks. 
We can observe this by adding a promise to the try block. It will return an error via Promise.reject:

(function testAsyncError() {
  try {
    console.log("Function execution started");
    return Promise.reject(new Error("Something went wrong..."));
  } catch (err) {
    console.log(`Error ${err.name} with the message ${err.message} has occured, but we've handled it`);
  }
  console.log("Function execution completed successfully");
})();
When we run this code, we'll see that the error has not been caught, and our code has crashed.


The error was not caught, and the code crashed

Fortunately, there is a way to fix this behavior. We can use the async/await syntax.

Error handling in asynchronous code: async/await
The purpose of the async/await syntax is to enable the engineer to write asynchronous code in a synchronous style. Simply put, it tells the JS engine where to "wait" for the result of a promise before proceeding any further in the code.

Let's rewrite the code from the example above using the async/await syntax.

For more clarity, we'll move the code that returns a promise with an error into a separate function returnPromiseError(). For the await expression to work inside a function, we need to add the async keyword to the function declaration. In the code below, we'll add it to the testAsyncAwaitError() function. Likewise, we'll add await before returnPromiseError().

As a result, we end up with the following code:

// Moved the code returning the promise with an error to an external function
function returnPromiseError() { 
  return Promise.reject(new Error("Something went wrong...")); 
}

(async function testAsyncAwaitError() {
  try {
    console.log("Function execution started");
    await returnPromiseError(); // wait till returnPromiseError() is executed
  } catch (err) {
    console.error(`${err.name} with the message ${err.message} has occured, but we've handled it`);
  }
  console.log("Function execution completed successfully");
})();
We're now ready to run the code in the browser console.

This time the function successfully executes and we can see our console has logged our success message: "Function execution completed successfully."


Successful function execution with error handling

In addition, the error itself goes into the handler inside the catch block and is handled accordingly (in our case, it's logged to the console). Success!

This is not the only way to handle errors that occur in promises. Let's move on to the next one.

Error handling in promises: the .catch handler
Another approach is to use the .catch promise handler — you're already familiar with that too. Here's how it works: if an error occurs when executing code in a promise, the error goes to the nearest .catch block described after it.

It may sound a bit complicated, but it's easier to understand with an example:

// Moved the return code of a promise with an error to an external function
function returnPromiseError() { 
  return Promise.reject(new Error("Something went wrong..."));
}

(function testPromiseRejectHandler() {
  returnPromiseError();
})();
When we run this code, an error will occur when returnPromiseError() is called. It will be inside the testPromiseRejectHandler() function, and the code will crash.

To fix this, let's handle the error by adding a .catch handler at the end of the call:

function returnPromiseError() { 
  return Promise.reject(new Error("Error. Something went wrong..."));
}

(function testPromiseRejectHandler() {
  returnPromiseError();
  .catch((err) => {
    console.error(`Error ${err.name} with the message ${err.message} has occurred while executing the code, but we've handled it`);
  });
})();
The error that occurred while executing the code was successfully caught and handled.

Note that when handling a sequence of promise calls, there may be multiple .catch handlers in the chain. In this case, the error will be caught by the first block that matches the error. Therefore, you should list your catch blocks with the most specific errors first and less specific errors towards the end.

Using console.error() to log errors
You're already familiar with using console.error() to log error messages to the console. In the context of back-end development with Express, console.error() is useful for identifying issues that might occur with your server, such as connection errors or other unexpected behavior. It can also be employed to log errors related to specific routes or middleware functions.

Error handling in callbacks
When asynchronous functions in JS are implemented using callbacks, they handle errors a little differently.

JavaScript engineers have a convention where the result of function execution is returned to the callback as two arguments:

The error object (if it occurs)
The result of the operation
Let's look at an example to get a better idea of error handling in callbacks. We'll create a function that writes some text to a specified file. For this, we'll use the writeFile() method from the fs module:

const fs = require('fs');
// Function that writes some text to a specified file
function writeTextToFile(filename, text) {
  fs.writeFile(filename, text, function (err, res) {
    console.log(`fs.writeFile has ended with the following result: ${res}`);
  })
}

// Call the funtion and pass an incorrect file name - ''
writeTextToFile('', 'sometext');
To run this, and the following examples in this lesson, you'll need a Node.js interpreter. To run the example code, save it in any location on your computer and run it with the command node <file_name>.

Contrary to our expectations, upon running this code, we don't see an error. It looks like everything has worked correctly, but this isn't entirely true. This is because there is virtually no error handling in the code. The error object is passed as the first argument to the callback function. Its value is not checked in any way, and the error itself won't be handled.

The problem with callback functions is that missed errors inside them don't cause application crashes and often go unnoticed. Let's fix this by adding the appropriate check:

const fs = require('fs');
function writeTextToFile(filename, text) {
  fs.writeFile(filename, text, function (err, res) {
    // check that the error object is not empty
    if (err) {
      console.error(`An error has occurred while writing the file: ${err.message}`);
      // end the function execution if an error occurs
      return;
    }
    console.log(`fs.writeFile has ended with the following result: ${res}`);
  })
}

writeTextToFile('', 'sometext');
Now the error is handled correctly, and the corresponding message appears in the console: An error has occurred while writing the file: ENOENT: no such file or directory, open ''.

Error handling and Mongoose: the orFail() helper
When attempting to find a record with Mongoose, such as with findOne or findById, if the record is not found, instead of throwing an error, it will simply pass null into your .then handler:

Card.find({ _id: "507f1f77bcf86cd799439011" }) // some nonexistent ID
  .then((cardData) => {
    // incorrectly sends `null` back to the client with a 200 status!
    res.send(cardData);
  })
  .catch((error) => {
    // does not run because no error was thrown
  });
You could handle this by checking if (cardData == null) in .then and throwing an error there, but the orFail helper can streamline your code by running when no record is found:

Card.find({ title: "nonexistant card" })
  .orFail() // throws a DocumentNotFoundError
  .then((cardData) => {
    res.send(cardData); // skipped, because an error was thrown
  })
  .catch((error) => {
    // now this does run, so we can handle the error and return an appropriate message
  });
You can also pass in a custom function to the orFail method:

.orFail(() => {
  const error = new Error("No card found with that id");
  error.statusCode = 404;
  throw error; // Remember to throw an error so .catch handles it instead of .then
})
Unfortunately, sometimes error handling is skipped and crashes happen. In the next lesson, we'll talk about global error handlers that are useful for preventing this, and we'll also learn to define our own error classes.

Next up, a small 3 question quiz to consolidate what we've just learned.
downloadDownload PNG downloadDownload JPEG downloadDownload SVG

Tip: You can change the style, width & colours of the snippet with the inspect tool before clicking Download!

Click to optimize width for Twitter