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:
<!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:
While in production, it does not contain the stack trace:
There are at least two major problems with this approach:
-
The default error HTTP status is
500
: That's too general and doesn't allow the caller to understand the reason behind the error. - 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
:
// 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:
// 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:
// 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 CustomError
s, 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:
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:
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:
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:
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:
"Hello, Serena!"
Now, omit the mandatory name
query parameter, as follows:
curl http://localhost:8080/api/v1/greetings/sayHi
You will get the following 400
error:
{
"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:
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.
Top comments (0)