javascript

How to Build an Error Handling Layer in Node.js

Antonello Zanini

Antonello Zanini on

How to Build an Error Handling Layer in Node.js

In an API-driven world, error handling is integral to every application. You should have an error handling layer in your Node.js app to deal with errors securely and effectively.

In this article, we'll explore:

  • What an error handling layer is in Node.js
  • Why your Express app should have an error handling layer, and how to implement it
  • Why you might need an advanced APM tool like AppSignal

Let’s jump right in!

What Is an Error Handling Layer?

An error handling layer is a collection of files in a backend software architecture that contain all the logic required to handle errors. Specifically, an error handling layer in a Node.js app can be implemented with an Express middleware that takes charge of intercepting and handling errors consistently.

In other words, all errors that occur in an Express application pass through that layer. By centralizing error handling logic in one place, you can easily integrate custom error behaviors like logging or monitoring. Also, you can standardize the error response returned by the Express server when an error occurs.

In short, an effective error response should contain:

  • A relevant error message: To help frontend developers present the error to users.
  • A timestamp: To understand when the problem happened.
  • A link to the documentation: To provide the caller with a useful resource to learn more about your application.
  • The stack trace: To be shown only in development to make debugging easier.

All this information helps the caller understand what happened and how to handle the error. These are just a few reasons why you should adopt an error handling layer in your Node.js Express application. Let's find out more about this.

Default Error Handling in Express

When an error occurs in an Express app, a 500 Internal Server Error response is returned by default.

That contains the following HTML error page:

html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <title>Error</title> </head> <body> <pre> <!-- Stack trace related to the error occurred in stage --> <!-- or --> <!-- HTTP error message --> </pre> </body> </html>

In staging, the error message looks as follows:

An example of the default error page returned by Express in stage

While in production, it does not contain the stack trace:

An example of the default error page returned by Express in production

There are at least two major problems with this approach:

  1. The default error HTTP status is 500: That's too general and doesn't allow the caller to understand the reason behind the error.
  2. The error response is in HTML: That's a browser-friendly format but much less versatile than JSON.

This is why you need an error handling layer in Express. Let's see how to implement it.

Why You Need an Error Handling Layer in Express

An error handling layer can bring several benefits to your backend Express application. Let's dig into the three most important ones:

Easier Debugging

The error handling layer makes it easier to implement logging and monitoring tools to better track errors over time. This helps you identify, debug, and study common and rare issues.

Improved User Experience

Standardizing the error response makes it easier for frontend developers to handle errors and present them correctly to end users. Using the error message and status code returned from the server, frontend developers will have enough data to inform users about any errors.

Increased Security

Centralizing error handling means that all errors go through the same place. So, all errors can be handled in the same way. This makes it easier to apply global security policies to remove sensitive information that attackers might use from error responses.

Implementing an Error Handling Layer in Express

Follow this step-by-step tutorial to add an error handling layer to your Express app.

Creating a Custom Error Class

To customize your error messages and gain better control over how your app handles errors, you can create a custom error handler by extending the Error class. Below, we name our class CustomError:

JavaScript
// src/errors/CustomError.js class CustomError extends Error { httpStatusCode; timestamp; documentationUrl; constructor(httpStatusCode, message, documentationUrl) { if (message) { super(message); } else { super("A generic error occurred!"); } // initializing the class properties this.httpStatusCode = httpStatusCode; this.timestamp = new Date().toISOString(); this.documentationUrl = documentationUrl; // attaching a call stack to the current class, // preventing the constructor call to appear in the stack trace Error.captureStackTrace(this, this.constructor); } } module.exports = { CustomHttpError: CustomError, };

CustomError extends Error with useful information to describe the error and support the API caller. Its properties will be used in the error handling layer to produce a proper error response. Keep in mind that message should always be rather general to avoid giving too many details to a potential attacker.

You can then use the custom error handler in your code, as shown below:

JavaScript
// src/controllers/greetings.js const { CustomHttpError } = require("../errors/CustomError"); const GreetingController = { sayHi: async (req, res, next) => { try { // read the "name" query parameter let name = req.query.name; if (name) { res.json(`Hello, ${name}!`); } else { // initialize a 400 error to send to the // error handling layer throw new CustomHttpError( 400, `Required query parameter "name" is missing!` ); } } catch (e) { // catch any error and send it // to the error handling middleware return next(e); } }, }; module.exports = { GreetingController, };

The sayHi() function in the GreetingController object contains the business logic associated with an API endpoint. Specifically, the sayHi API expects to receive the name parameter in the query string. If name is missing, a CustomHttpError is raised.

Note the 400 passed to CustomHttpError in the constructor. That HTTP status will be used by the error handling layer to return a 400 Bad Request error instead of a generic 500 error. Let's now learn how to implement the Express middleware that encapsulates the error handling logic.

Centralizing Error Handling in Middleware

The best place to centralize error handling logic in Express is in middleware. If you aren't familiar with this concept, an Express middleware is a function that acts as a bridge between an incoming request and the final response.

Middleware provides ways to:

  • process the request data before forwarding it to the business logic
  • manipulate the response before sending it to the client

In summary, middleware allows you to intercept errors that occur during the request-response cycle and handle them as desired.

You can define an error handling middleware function as follows:

JavaScript
// src/middlewares/errorHandling.js const { CustomHttpError } = require("../errors/CustomError"); function errorHandler(err, req, res, next) { // default HTTP status code and error message let httpStatusCode = 500; let message = "Internal Server Error"; // if the error is a custom defined error if (err instanceof CustomHttpError) { httpStatusCode = err.httpStatusCode; message = err.message; } else { // hide the detailed error message in production // for security reasons if (process.env.NODE_ENV !== "production") { // since in JavaScript you can also // directly throw strings if (typeof err === "string") { message = err; } else if (err instanceof Error) { message = err.message; } } } let stackTrace = undefined; // return the stack trace only when // developing locally or in stage if (process.env.NODE_ENV !== "production") { stackTrace = err.stack; } // logg the error console.error(err); // other custom behaviors... // return the standard error response res.status(httpStatusCode).send({ error: { message: message, timestamp: err.timestamp || undefined, documentationUrl: err.documentationUrl || undefined, stackTrace: stackTrace, }, }); return next(err); } module.exports = { errorHandler, };

The errorHandler() function takes care of defining the error handling logic. In particular, it initially marks the error to return a generic 500 Internal Server Error. Then, if the error is of type CustomError or you are not in production, it reads the specific error message. This way, Express will always return secure error messages that do not contain too much information in production.

In the case of unspecified errors, the system will describe the error with the "Internal Server Error" message. When it comes to CustomErrors, the backend will describe the error with the generic message string you passed to the custom error class constructor. Also, the error response reads the HTTP status code and other info from the CustomError instance.

You are now in control of what is returned by the Express server when an error occurs. As you can see, the returned JSON response is now consistent regardless of error type.

Error Handling Layer in Action

Register the errorHandler() middleware function by adding the following lines to your index.js file:

JavaScript
const { errorHandler } = require("./middlewares/errorHandling"); // ... app.use(errorHandler);

First, import the errorHandler() function and then add it to your Express app through the use() method.

The error handling layer is now ready to be tested! Clone the GitHub repository that supports this article:

Shell
git clone https://github.com/Tonel/custom-error-handling-nodejs cd custom-error-handling-nodejs

Then, install the local dependencies and start the local server with:

Shell
npm install npm run start

The demo Express app with an error handling layer should now be running at http://localhost:8080.

Call the sayHi sample API with the command below:

Shell
curl http://localhost:8080/api/v1/greetings/sayHi?name=Serena

If you do not have curl installed, visit http://localhost:8080/api/v1/greetings/sayHi?name=Serena in your browser, or perform a GET request in your HTTP client.

In all cases, you will get:

JSON
"Hello, Serena!"

Now, omit the mandatory name query parameter, as follows:

Shell
curl http://localhost:8080/api/v1/greetings/sayHi

You will get the following 400 error:

JSON
{ "error": { "message": "Required query parameter \"name\" is missing!", "timestamp": "2023-02-06T14:24:07.678Z", "stackTrace": "Error: Required query parameter \"name\" is missing!\n at sayHi ..." } }

This JSON content matches exactly the error response defined in the error handling layer. Also, note that the error returned by the Express server is no longer a generic 500 error.

Et voilà! Your Express error handling layer works as expected.

Tracking Errors with AppSignal

To track errors, you can include an Application Performance Management, or APM, tool in your error handling layer.

AppSignal is a performance monitoring and error tracking platform that provides real-time information on application performance, stability, and error rates. After you integrate it into your application, AppSignal will begin collecting data to give you access to contextual information about each error event, detailed error traces, and performance metrics. This will help you identify, diagnose, and fix bugs in your application.

AppSignal supports Node.js and you can integrate it into your Express app in minutes. Check out our Node.js docs for installation instructions.

After integrating AppSignal into your app, it will automatically track errors for you. But note, only exceptions with status code 500 and above will be reported to AppSignal automatically.

To send exceptions to AppSignal with other status codes, use setError and sendError in your custom error handler. See our custom exception handling guide for more.

Here's how an Express error might look in AppSignal:

Express error details

Read about AppSignal for Express.

Wrapping Up

In this blog post, you saw how to build an architecture layer to bring the error handling logic of your Node.js Express backend to the next level. You learned:

  • How Express handles errors by default
  • What a good error response should look like
  • Why your Express app needs an error handling layer
  • How to implement an error handling layer in Express

Thanks for reading, and see you in the next one!

P.S. If you liked this post, subscribe to our JavaScript Sorcery list for a monthly deep dive into more magical JavaScript tips and tricks.

P.P.S. If you need an APM for your Node.js app, go and check out the AppSignal APM for Node.js.

Antonello Zanini

Antonello Zanini

Guest author Antonello is a software engineer, but prefers to call himself a Technology Bishop. Spreading knowledge through writing is his mission.

All articles by Antonello Zanini

Become our next author!

Find out more

AppSignal monitors your apps

AppSignal provides insights for Ruby, Rails, Elixir, Phoenix, Node.js, Express and many other frameworks and libraries. We are located in beautiful Amsterdam. We love stroopwafels. If you do too, let us know. We might send you some!

Discover AppSignal
AppSignal monitors your apps