Introduction
Exceptions and errors are bound to occur while users interact with any application, it is up to software engineers to choose a means to handle any error that might arise - knowingly or unknowingly. As a result, backend developers who build APIs with Express find themselves working to ensure that they are building a useful, efficient, and usable API. What is of most importance is to handle errors in such a way as to build a robust system because this helps to reduce development time, outright errors, productivity issues, and determines the success or scalability of software development.
Do you need to log the error message, suppress the error, notify users about the error, or write code to handle errors? Wonder no more.
In this guide, we will learn how to build a robust error-handling codebase for Express applications, which will serve in helping to detect application errors and take optimal actions to recover any application from gracefully failing during runtime.
Note: We'll be using Postman to test the API in our demo. You can download it on the Postman Download page. Alternatively, you can simply use the browser, the command-line curl
tool, or any other tool you might be familiar with.
What is Error Handling?
In software development, there are two different kinds of exceptions: operational and programmatic.
- Operational failures might arise during runtime, and in order to prevent the application from terminating abruptly, we must gracefully handle these exceptions through efficient error handling methods.
- Programmatic exceptions are thrown manually by a programmer, when an exceptional state arises.
You can think of operational exceptions as "unexpected, but foreseen" exceptions (such as accessing an index out of bounds), and programmatic exceptions as "expected and foreseen" exceptions (such as a number formatting exception).
Exception handling is the procedure used to find and fix flaws within a program. Error handling sends messages that include the type of error that happened and the stack where the error happened.
Note: In computer science, exceptions are recoverable from, and typically stem from either operational or programmatic issues during runtime. Errors typically arise form external factors, such as hardware limitations, issues with connectivity, lack of memory, etc. In JavaScript, the terms are oftentimes used interchangeably, and custom exceptions are derived from the Error
class. The Error
class itself represents both errors and exceptions.
In Express, exception handling refers to how Express sets itself up to catch and process synchronous and asynchronous exceptions. The good thing about exception handling in Express is that as a developer, you don't need to write your own exception handlers; Express comes with a default exception handler. The exception handler helps in identifying errors and reporting them to the user. It also provides various remedial strategies and implements them to mitigate exceptions.
While this might seem like a lot of stuff going under the hood, exception handling in Express doesn't slow the overall process of a program or pause its execution.
Understanding Exception Handling in Express
With the default error handler that comes with Express, we have in our hands a set of middleware functions that help to catch errors in route handlers automatically. Soon, we will create a project to put theory into practice on how to return proper errors in an Express app and how not to leak sensitive information.
Defining middleware function in Express
The error-handling middleware functions are defined in such a way that they accept an Error
object as the first input parameter, followed by the default parameters of any other middleware function: request
, response
, and next
. The next()
function skips all current middleware to the next error handler for the router.
Setting up Error Handling in Express
Run the following command in your terminal to create a Node and Express app:
$ mkdir error-handling-express
In the newly created folder, let's initialize a new Node project:
$ cd error-handling-express && npm init -y
This creates a package.json
file in our folder.
To create an Express server in our Node app, we have to install the express
package, dotenv
for automatically loading environment variables into .env
file into process.env
object, and nodemon
for restarting the node app if a file change is noted in the directory.
$ npm install express dotenv nodemon
Advice: If you'd like to read more about dotenv
or nodemon
, you can read our "Guide to Managing Environment Variables in Node.js with dotenv" and "Restarting Node.js Apps on File Change with nodemon"
Next, create an app.js
file in the project folder which will serve as the index file for the app.
Now that we have installed all the needed dependencies for our Express app, we need to set up the script for reading the app in the package.json
file. To achieve that, the package.json
file, so that the scripts
object is like shown below:
"scripts": {
"start": "nodemon app.js"
},
Alternatively, you can skip using nodemon
, and use node app.js
instead.
Setting up an Express server
To set up the server, we have to first import the various packages into app.js
. We will also create a .env
file in the project directory - to store all environment variables for the application:
// app.js
const express = require('express')
require('dotenv').config
//.env
PORT=4000
We have defined the port number for the app in .env
, which is loaded in and read by dotenv
, and can be accessed later.
Initializing the Express Server
Now, we need to initialize the Express server and make our app listen to the app port number, along with a request to a test route - /test
. Let's update app.js
, beneath the import statements:
// app.js
const app = express();
const port = process.env.PORT || 4000;
app.get("/test", async (req, res) => {
return res.status(200).json({ success: true });
});
app.listen(port, () => {
console.log(`Server is running at port ${port}`);
});
From here on, we will learn how to handle various use cases of operational errors that can be encountered in Express.
Handling Not Found Errors in Express
Suppose you need to fetch all users from a database of users, you can efficiently handle a potential error scenario where no data exist in the database, by wrapping the logic into a try/catch
block - hoping to catch any error that could project in the catch
block:
//app.js
const getUser = () => undefined;
app.get("/get-user", async (req, res) => {
try {
const user = getUser();
if (!user) {
throw new Error('User not found');
}
} catch (error) {
// Logging the error here
console.log(error);
// Returning the status and error message to client
res.status(400).send(error.message)
}
return res.status(200).json({
success: true
});
});
This results in:
User not found
Now, when this request is made (you can test using Postman) and no user exists on the database, the client receives an error message that says "User not found". Also, you will notice that the error is logged in the console, too.
Optimizing Error Handling with Error Handler Middleware
We can optimize development by creating an error handler middleware that would come at the end of all defined routes, so that if an error is thrown in one of the routes, Express will automatically have a look at the next middleware and keep going down the list until it reaches the error handler. The error handler will process the error and also send back a response to the client.
Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!
To get started, create a folder called middleware
in the project directory, and in this folder, create a file called errorHandler.js
which defines the error handler:
const errorHandler = (error, req, res, next) => {
// Logging the error here
console.log(error);
// Returning the status and error message to client
res.status(400).send(error.message);
}
module.exports = errorHandler;
In our middleware function, we have made Express aware that this is not a basic middleware function, but an error handler, by adding the error
parameter before the 3 basic parameters.
Now, we'll use the error handler in our demo app.js
and handle the initial error of fetching users with the error handler middleware, like shown below:
// app.js
const getUser = () => undefined;
app.get("/get-user", async (req, res, next) => {
try {
const user = getUser();
if (!user) {
throw new Error("User not found");
}
} catch (error) {
return next(error);
}
});
app.use(errorHandler);
We can optimize our code even more, by creating an abstraction around the try/catch
logic. We can achieve this by creating a new folder in the project directory called utils
, and in it, create a file called tryCatch.js
.
To abstract the try-catch
logic - we can define a function that accepts another function (known as the controller) as its parameter, and returns an async
function which will hold a try/catch
for any received controller.
If an error occurs in the controller, it is caught in the catch
block and the next function is called:
// tryCatch.js
const tryCatch = (controller) => async (req, res, next) => {
try {
await controller(req, res);
} catch (error) {
return next(error);
}
};
module.exports = tryCatch;
With the try/catch
abstraction, we can refactor our code to make it more succinct by skipping the try-catch
clause explicitly when fetching users in the app.js
:
// app.js
const getUser = () => undefined;
app.get(
"/get-user",
tryCatch(async (req, res) => {
const user = getUser();
if (!user) {
throw new Error("User not found");
}
res.status(400).send(error.message);
})
);
We have successfully abstracted away the try-catch logic and our code still works as it did before.
Handling Validation Errors in Express
For this demo, we will create a new route in our Express app for login - to validate a user ID upon login. First, we will install the joi
package, to help with creating a schema, with which we can enforce requirements:
$ npm i joi
Next, create a schema which is a Joi.object
with a userId
which must be a number and is required - meaning that the request must match an object with a user ID on it.
We can use the validate()
method in the schema object to validate every input against the schema:
// app.js
const schema = Joi.object({
userId: Joi.number().required(),
});
app.post(
"/login",
tryCatch(async (req, res) => {
const {error, value} = schema.validate({});
if (error) throw error;
})
);
If an empty object is passed into the validate()
method, the error would be gracefully handled, and the error message would be sent to the client:
On the console, we also get access to a details
array which includes various details about the error that could be communicated to the user if need be.
To specifically handle validation errors in such a way as to pass the appropriate error detail per validation error, the error handler middleware can be refactored:
// errorHandler.js
const errorHandler = (error, req, res, next) => {
console.log(error); // logging the error here
if (error.name === "ValidationError") {
return res.status(400).send({
type: "ValidationError",
details: error.details,
});
}
res.status(400).send(error.message); // returning the status and error message to client
};
module.exports = errorHandler;
With errorHandler.js
now customized, when we make the same request with an empty object passed to the validate()
method:
We now have access to a customized object that returns messages in a more readable/friendly manner. In this way, we are able to send and handle different kinds of errors based on the kind of error coming in.
Conclusion
In this guide, we went over every aspect of Express.js' error handling, including how synchronous and asynchronous code is handled by default, how to make your own error classes, how to write custom error-handling middleware functions and provide next
as the final catch handler
As with every task out there, there are also best practices during development which includes effective error handling, and today we have learned how we can handle errors in an Express app in a robust manner.
Handling errors properly doesn't only mean reducing the development time by finding bugs and errors easily but also developing a robust codebase for large-scale applications. In this guide, we have seen how to set up middleware for handling operational errors. Some other ways to improve error handling includes: not sending stack traces, stopping processes gracefully to handle uncaught exceptions, providing appropriate error messages, sending error logs, and setting up a class that extends the Error
class.
I hope the examples I used in this tutorial were enjoyable for you. I covered various scenarios you could potentially encounter when writing an Express application for use in the real world about error management. Please, let me know if there is anything I missed. It will benefit us and help me learn more as well. Have a good day and thanks for reading.
You can refer to all the source code used in the article on GitHub.