Guide to Exception Handling in Express PlatoBlockchain Data Intelligence. Vertical Search. Ai.

Guide to Exception Handling in Express

Introduction

Exceptions and error 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 these 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

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:



const express = require('express')
require('dotenv').config

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:


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:


const getUser = () => undefined;

app.get("/get-user", async (req, res) => {
	try {
		const user = getUser();
		if (!user) {
			throw new Error('User not found');
		}
	} catch (error) {
	    
		console.log(error); 
		
		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) => {
    
    console.log(error); 
    
    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:


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 abtsraction 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 receieved controller.

If an error occurs in the controller, it is caught in the catch block and the next function is called:


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 succint by skipping the try-catch clause explicitly when fetching users in the 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 log in. 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:


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:


const errorHandler = (error, req, res, next) => {
	console.log(error); 

	if (error.name === "ValidationError") {
		return res.status(400).send({
			type: "ValidationError",
			details: error.details,
		});
	}

	res.status(400).send(error.message); 
};

module.exports = errorHandler;

With errorHandler.js now customized, when we make the same request with an empty object passed to the validate() method:

Guide to Exception Handling in Express PlatoBlockchain Data Intelligence. Vertical Search. Ai.

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.

Additional Resources

Time Stamp:

More from Stackabuse