A complete beginner friendly guide to Error Handling in Node.js - Zero to Hero

A complete beginner friendly guide to Error Handling in Node.js - Zero to Hero

To provide the best user experience in your applications, it is crucial to ensure that they behave predictably and have a proper error-handling mechanism in place. This will enable you to provide meaningful error messages to both users and developers, rather than generic messages such as "Something Went Wrong."

Proper error handling not only improves the user experience, but also helps to enhance application performance and security. It can prevent the entire application from crashing in production. In this blog post, we will explore different ways to handle errors in Node.js.

Handle errors gracefully

Before learning how to handle errors, it is essential to understand where and how errors can occur in Node.js applications, as well as the types of errors that we can handle and those that we cannot.

Let's try to understand the behavior of Node.js in case and error occurs.

In Node.js, instances of the Error class are used to throw errors that may occur during program execution.

Error class is a built-in JavaScript class that represents an error. When an error is created using the Error class, it captures details such as the error message and the stack trace, which can be used for debugging purposes.

The Error class include following properties.

  • message: Containing the error message.
  • name: Representing the type of the error. By default, this is "Error".
  • stack: A stack trace to help debug where the error originated.

Stack trace is useful for developers to find where the error occur. It gives a details path that program took leading up-to errors, including filenames, lines numbers, and function names.

Topically there are two types of errors in Nodejs.

  • Programmer errors
    • Syntax Error
    • Logical Errors
  • Operational errors 
    • Asynchronous errors
    • Synchronous Errors

Programmer errors are the type of error that occur due to the mistake of logical or syntax in the program. Syntax Errors are the examples for programmer errors.

Operational errors are the errors that occur due to the external factor or user actions. Database queries returning no results is an simple example for operational errors.

Programmer Errors

"Programmers: turning coffee into code and bugs into features."

Programming errors are the mistakes done by programmer. You need to handle them during development. The tools likes ESLint is helpful to handle this type of errors.

Syntax Errors

Syntax errors are the type of programmer error which occur when the code violates the syntax rules of the programming language. In JavaScript, syntax errors are detect by the JavaScript engine during the parsing stage, before the code is executed.

A good code editor can identify theses types of errors before run the code. We are recommended to use Visual Studio Code for JavaScript.

The important factor is you can't caught syntax errors by try/catch blocks which we discus later in this post.

Logical Errors

Logical errors occur when a program runs without crashing but produces incorrect or unexpected results.

These errors arise from wrong program's logic and can be particularly tricky because they do not trigger exceptions or immediate failures. Instead, they manifest through incorrect behavior, which can sometimes go unnoticed until it's too late.

Incorrect conditional logic, infinite loops, and attempts to divide by zero are common examples of logical errors.

I have implemented a function called findBookDatabase() which simulates the process of finding a book from a database. This function has a 50% chance of successfully finding the book. If the book is found, the function will return the book object. Otherwise, it will return null.

function findBookFromDatabase() {
  const random = Math.random();
  if (random > 0.5) {
    // simulate book found
    return {
      name: "Harry Potter",
      author: "J.K. Rowling",
      price: 100,
    };
  } else {
    // book not found
    return null;
  }
}

Then let's call the function and search the book

const book = findBookFromDatabase(name);
console.log(book.name);

Although the current implementation seems to be working, there is a potential issue when the book is not found. In this case, the code throws an error because the name property cannot be accessed from null.

This type of logical error can be avoided by using a simple if statement.

if(book){
  console.log(book.name);
}else{
  console.log("Book not found");
}

This is just one example for logical error and to avoid programmer errors, it is important to carefully review the code and perform thorough testing to identify any potential issues.

Tip : You can also try Typescript which gives you a better type checking for identify errors earlier.

Operational Errors

Operational errors are happen during the application is working, There are several ways to handle this types of errors .

To handle operational errors, one can use a try..catch block. This block is allow you to catch any errors that are thrown by the program. as well as the errors that manually thrown by programmer.

In order to understand error thrown and catching process let's implement the same example that we used eailer.

function findBookFromDatabase() {
  const random = Math.random();
  if (random > 0.5) {
    // simulate book found
    return {
      name: "Harry Potter",
      author: "J.K. Rowling",
      price: 100,
    };
  } else {
    throw new Error("Book Not found!");
  }
}

I have made some changes to the function we previously used. The function now throws and error, instead of returning null, which can be easily handle using try catch block. But before discussing this further, let's take a moment to understand the error thrown process.

More about Error thrown process.

Throwing an error in programming is the process of generating an error intentionally to signal that something has gone wrong in the code.

This helps in identifying and handling unexpected conditions or bugs.

In JavaScript, you can throw an error using the throw statement:

throw new Error("New Error");

In JavaScript, if an error occurs during program execution, it can cause the program to crash.

To prevent this from happening, you can handle the error by using a try...catch block. This block allows you to attempt a piece of code and then catch any errors that may occur, allowing you to handle them appropriately.

Here is our new implementation of calling the function with try catch block.

  try {
    const book = findBookFromDatabase(name);
    console.log(book);
  } catch (error) {
    console.log("Book Not Found");
  }

In this updated code, I have implement try catch logic to handle, or catch if any error is happen. By using this method we will be able to handle the operational errors.

In some scenarios, when we use the same try-catch multiple times, we end up repeating ourselves in the code. To avoid this, we can use a wrapper function that handles errors.

function asyncHandler(fn) {
  return function (req, res, next) {
    Promise.resolve(fn(req, res, next)).catch(next);
  };
}

module.exports = asyncHandler;

Or if you are using Express.js you can use following code to which use Express.js special error handling middleware next().

function asyncHandler(func) {
  return function (req, res, next) {
    func(req, res, next).catch(next);
  };
}

module.exports = asyncHandler;

also you can implement error handling middle-ware in Express.js to handle errors easier.

const errorHandler = (err, req, res, next) => {
    // Handle the error and send an appropriate response
    res.status(err.status || 500).json({
        error: {
            message: err.message || 'Internal Server Error'
        }
    });
};

Use Cases for Global Error Handlers (process.on)

Global error handlers are essential in handling uncaught errors that are not detected by local error handling mechanisms. These error handlers play a vital role in maintaining the stability of your application which provides a final layer of defense against crashes.

Uncaught Exceptions

Handling uncaught exceptions ensures that unexpected errors do not crash your application without some form of logging or cleanup.

process.on('uncaughtException', (error) => {
  console.error('Uncaught Exception:', error);
  // Perform cleanup or logging
  process.exit(1);
});

// Example of an uncaught exception
setTimeout(() => {
  throw new Error('This will trigger uncaughtException');
}, 1000);

Unhandled Promise Rejections

Handling unhandled promise rejections ensures that rejected promises do not cause your application to behave unexpectedly:

process.on('unhandledRejection', (reason, promise) => {
  console.error('Unhandled Rejection at:', promise, 'reason:', reason);
  // Perform cleanup or logging
  process.exit(1);
});

// Example of an unhandled promise rejection
setTimeout(() => {
  Promise.reject(new Error('This will trigger unhandledRejection'));
}, 2000);

Handle errors with array.

In Node.js, It is a common practice to return both a result and an error in a function. This is typically done by leveraging the array destructuring feature provided by Node.js.

Here is a example of such a function.


function doSomething(){
  try{
    const data = fetch("https://api.com");
    return [null, data];

  }catch(error){
    return [error, null]
  }
}

and we call the above function as follows.

const [data,error] = await doSomething();

Next, let's look at how to implement our own custom class instead of using the generic Error class in Nodejs.

Custom Error Class

Nodejs allows you to implement custom error handling class to change according to your use case. These custom errors provide extensive error management by providing more contextual information about the error which can be used to debug the issue faster.


class AppError extends Error {
    constructor(message, statusCode) {
        super(message);
        this.statusCode = statusCode;
        this.name = 'AppError';
    }

You can check whether an error is of a specific error class type using the instanceof operator. This operator checks whether an object is an instance of a specific class or not.

if (errr instance of AppError){
    // do something
} 

Building a centralized error-handling component is a good idea to avoid unnecessary duplication of error-handling code. This approach allows for a more efficient management of errors across the application and reduces the chance of errors slipping through unnoticed.

I hope that now you got an good overall idea about how to handle error in Nodejs and we will share more indepth best practices in our upcomming posts.

meanwhile try above simple quiz.

Test your knowledge

Can you guess that output of following code chuck using the things that you have learned ? What did you thing can the catch block handle the error that throw by try block.

If you like to discuss above topic more, feel free come and post it on our Facebook community.

source: Nodejs Official Documentaion