For Developers

How to Master Express JS Error Handling?

How to Master Express JS Error Handling

Building a web app or API with Express JS is mainly done for high-quality error handling. Error handling is one of the most underlooked yet important aspects of creating good code for your end users. In this article, we will discuss the Express JS error handling best practices and help you manage errors efficiently.

How Express JS error handling works

Expresses built-in error handling

Express JS performs the basic error handling task by default. When writing synchronous code Express will catch your errors and automatically create a 500 response with the error message.

app.get("/", (req, res) => {
	throw new Error("My error message here.");
})

The response will look something like this.

Express js error handling.webp

Express JS error handling middleware

The problem

Although the method shown above will work for basic examples using synchronous code, however, asynchronous code is a whole different beast. When the following code is used, the app does not automatically handle errors and will crash.

app.get("/", async (req, res) => {
	throw new Error("My error message here.");
})

The reason behind this is when a request is handled under the hood, express runs a try-catch statement in an error handling middleware shown in the code below.

try {
       fn(req, res, next);
} catch (err) {
	 next(err);
}

If you have ever error handled using the asynchronous functions the issue should now be clear. When throwing an error in an async function, it returns a promise that has been rejected rather than directly calling throw in the try statement.

How do we solve this

The solution to this problem is to run the next function directly instead of throwing an error. This ensures that the error middleware is called with the error.

app.get("/", async (req, res) => {
	return next(new Error("My error message here."))
})

If you are using promise chaining you can also put the next function in the catch statement.

app.get("/", async (req, res) => {
	promise()
		.then(() => {
			// do a thing	
		})
		.catch(next)
})

Writing your middleware

When writing your app you may need some more logic to handle your errors such as error loggers and custom error messages to your client. An error middleware is a function that will include error, request, response, and the next function as parameters.

An example of a middleware is an error logger. In its simplest form, it will log the error to the console and then pass the error to the next middleware. Below is a simple example of how you may implement this.

const errorLogger = (err, req, res, next) => {
	console.error(err.stack);
	next(err);
}

To use the middleware you can pass it into the “app.use” method after that you can include your routes.

Note: this order is essential.

// use your routes in your app
app.use(routers)
// then use your middlewares
app.use(errorLogger)

Another middleware that you can also use is an error handler that controls the error sent to the client. This can be important if you want to respond with an error in a specific format and response type. The error handler middleware will be at the end of the middleware error chain since it has to be run after all other middleware.

const errorHandler = (err, req, res, next) => {
	const statusCode = err.statusCode ?? 500;

	return res.status(statusCode ?? 500).json({
		error: statusCode,
		message: err.message
	})
}

You can include this handler by using it in your app below the error logger handler in the code.

Custom Errors

Custom error types are very useful when you write specific functionality or include specific attributes such as status codes in your error.

class CustomError extends Error {
	constructor(reason) {
		this.name = this.constuctor.name;
		this.message = `Custom error occured due to ${reason}.`;
		this.statusCode = 500;
	}
}

Express JS error handling 404

You can catch all responses that are not sent by adding a middleware at the end of the chain with a request, response, and next parameters.

const notFoundHandler = (req, res, next) => {
res.status(404).json({
		error: 404,
		message: "Route not found."
	})
}

app.use(notFoundHandler);

If you have a custom error handler middleware you can alternatively create a custom error type with a status code of 404 and an appropriate message.

Real-world example

To understand how you can use these techniques in a real project it's good to practice by creating an example project. In this example, we will show how you can create a book API using scalable error-handling techniques.

Creating our middleware

As mentioned in the previous section, we can create middleware to handle the errors. In this example, we will use 3 main error-handling middleware. This will include middleware for logging errors, handling errors, and handling 404 errors.

We will put the middleware in a directory “src/middlewares”. First, we have an error logger. This is almost the same as in the previous example and is nothing fancy. You can also save the output in a file.

// "src/middlewares/errorLogger.js"

const errorLogger = (err, req, res, next) => {
	// logs error to console and then passes to next middleware
  console.error(err.stack);
  next(err);
}

module.exports = errorLogger;

Next, we have the error handler. This middleware will take an error with a message and potential status code and return it to the client in a JSON format. This will be the last middleware in the chain as it is always the last thing to run.

// "src/middlewares/errorHandler.js"

const errorHandler = (err, req, res, next) => {
	// if no status or message in error use default 500 code and message
	const statusCode = err.status ?? 500;
	const message = err.message || "Something went wrong.";

	// returns error status code and message
	return res.status(statusCode).json({
		error: statusCode,
		message: message
	})
}

module.exports = errorHandler;

The last error middleware is a 404 not found error handler. We can do this by passing an error with the status code 404 to the error handler. To do this we will need to create a custom error with the status code 404 and the message “Not found”. We can put all our custom errors in the directory “src/helper/errors”.

// "src/helpers/errors/NotFoundError.js"
class NotFoundError extends Error {
  constructor() {
    super();
    this.status = 404;
    this.message = "Not found."
  }
}

module.exports = NotFoundError;

The middleware can now simply pass an instance of a not found error to the error middleware.

// "src/middlewares/errorNotFound.js"

const NotFoundError  = require("../helpers/errors/NotFoundError")

const errorNotFound = (req, res, next) => next(new NotFoundError();

module.exports = errorNotFound;

To use these routes we can simply run the use method on the express app. In our app, we will also use the built-in express JSON middleware and the router at the address “/api”. Again, while creating this make sure the ordering is correct as the flow of the middleware is important.

const express = require("express");
const errorHandler = require("./middlewares/errorHandler");
const errorLogger = require("./middlewares/errorLogger");
const errorNotFound = require("./middlewares/errorNotFound");
const routes = require("./routes");

const app = express();

app.use(express.json());

app.use("/api", routes);

app.use(
  errorNotFound, 
  errorLogger, 
  errorHandler
);

module.exports = app;

The router

We put our router in the directory “src/routes”. Since we are only using one route in this example we will just put all the code in “src/routes/index.js” but it is best practice to put each route and controller into directories.

First, let us create the route with no Express JS error handling. We can use the “express.Router“ method to create a router and export it as the default value from the file.

const express = require("express");
const fs = require('fs');

const router = express.Router();

module.exports = router;

For this API we will create the route “/find”, which will take a post request with a JSON body containing a filename and title for the book. We will store JSON files in a “data” directory and read them with the inbuilt fs library in node js. The fs.readFile method takes the encoding and a callback with an error and data parameter in the file path.

fs.readFile(`data/${filename}.json`, 'utf8', (err, data) => {/*code*/})

In the callback, we will first parse the data as a JSON object. We can do this with the JSON.parse method as shown below.

const json = JSON.parse(data);

We now need to filter the data for books with a given title. We can use the filter method that you can call on arrays in JavaScript.

const books = json.books.filter(
  val => val.title.toLowerCase().includes(title.toLowerCase()));

Lastly, we return a response with a 200 status code and JSON object of the books.

return res.status(200).json({
  books
})

All together your file with no error handling should look as follows.

// "src/routes/index.js"

const express = require("express");
const fs = require('fs');

const router = express.Router();

router.post("/find", async (req, res, next) => {
  const {filename, title} = req.body;
    
  fs.readFile(`data/${filename}.json`, 'utf8', (err, data) => {
    const json = JSON.parse(data);

    const books = json.books.filter(
	    val => val.title.toLowerCase().includes(title.toLowerCase()));
	
	  return res.status(200).json({
      books
    })
  });
})

module.exports = router;

Now if we send a request with fields that won't cause an error when we get a response that looks like the following.

Express js error handling best practices.webp

Adding errors

There are 3 main places in this code where we may want to add error handling. The first will be to validate that the body does include the required parameters. We could resolve this directly in the route by checking if the parameters exist. A better way to do this is by creating a function that returns a middleware to check on the existence of the required parameters. The reason we do this is that this functionality will likely be used all over the code in many routes.

First, we will create a new custom error in “helper/errors” called RequiredBodyError. This error will be in the fields that are not included in the constructor. It will then set the status code to 400 and set the message to “Request must include the fields: (names of fields)”. This is simply done in the example below.

class RequiredBodyError extends Error {
    constructor(notIncludedFields) {
        super();
        this.status = 400;
        this.message = `Request must include the fields: ${notIncludedFields.join(', ')}.`;
    }
}

module.exports = RequiredBodyError;

We have our custom error that will create the function which will eventually create the middleware. To do this we will take in an array called fields which are the required fields and then check if the body has all of the fields. If the body doesn't have all the fields it will run the next error middleware with a RequiredBodyError with all the missing fields.

const RequiredBodyError = require("../helpers/RequiredBodyError");
const requiredFields = (fields) => {
  return (req, _res, next) => {
    const missingFields = [];
    const keys = Object.keys(req.body); // Included fields

		// Checks if every required field is in the body
    for(const field of fields)
      if(!keys.includes(field))
        missingFields.push(field)

		// If there are missing fields then run next error middleware
    if(missingFields.length)
      return next(new RequiredBodyError(missingFields));
    
		// If no missing fields then run router code
    return next();
  }
}

module.exports = requiredFields;

You can now go back to the router code and add this new functionality. You can add this function as a parameter to the “router.post” method just before the controller code with an array of required fields. When done correctly the code will look something like the following.

router.post("/find", requiredFields(["filename", "title"]), async (req, res, next) => {

If you request this route with no fields it should give an appropriate 400 response with a reason for the error.

Express js error handling 404.webp

The next 2 errors are very easy to handle. The first is just checking if there is an error reading the file. To do this we just check if the error parameter is not undefined and if so, return an error.

fs.readFile(`data/${filename}.json`, 'utf8', (err, data) => {
	if(err)
    return next(new Error(`Error reading file ${filename}.`));

The last error will check if the JSON can be parsed and whether the JSON is in the correct format. This can simply be done with a try-catch statement. In the catch, we will just return an error with no message since it's just a standard 500 error and we just want to signal that something went wrong. Once all error handling is implemented the final express js route error handling code will look like the following.

// src/routes/index.js

const express = require("express");
const fs = require('fs');
const requiredFields = require("../middlewares/requiredFields");

const router = express.Router();

// Post request at /find with a requiredFileds middleware
router.post("/find", requiredFields(["filename", "title"]), async (req, res, next) => {
	// Used fields  
	const {filename, title} = req.body;
    
  fs.readFile(`data/${filename}.json`, 'utf8', (err, data) => {
		// If error when reading the file return an error
    if(err)
      return next(new Error(`Error reading file ${filename}.`));

    try {
			// Parse file
      const json = JSON.parse(data);

      const books = json.books.filter(
        val => val.title.toLowerCase().includes(title.toLowerCase()));

			// Send response
      return res.status(200).json({
        books
      })
    } catch {
			// If could not parse file then return an error
      next(new Error());
    }
  });
})

module.exports = router;

If we now send a request with a file that does not exist, we get a response that looks like the following.

Express js route error handling.webp

If you send a request with a file that is not formatted correctly you will get a response that looks like the following.

A Guide to Error Handling in Express.js.webp

Conclusion

In this article, we explored the different approaches to error handling using middleware, custom errors, and async functions. We not only saw where the server can crash but also why it crashes if not handled correctly in async functions. Finally, we looked at how 404 errors can be handled and how we can cohesively implement it with the middleware and custom errors.

With this information, you can now create scalable projects that follow best practices when it comes to express error handling. Also, you are equipped with the skills to implement the ideas since we went through a guided example using all the concepts together.

Author

  • Author

    Liam Clegg

    Liam Clegg is a full-stack web developer from the UK with experience as a freelancer and writer in modern web techs such as React, NextJS, express and Postgress, and MongoDB.

Frequently Asked Questions

Avoid using “Try-catch” above a limit, leverage error objects in rules, and utilize meaningful error code descriptions to avoid error handling in JS.

You can deal with errors in Express JS by putting the error handling logic in the distinct route handler functions.

The three types of errors are random errors, systematic errors, and negligent errors. Try using the try-catch-finally statement to handle the exceptions in Javascript.

For example, we know that any non-zero value when divided by zero always results in infinity and is an exception. Exception handling takes care of it in one go without any hustle.

View more FAQs
Press

Press

What's up with Turing? Get the latest news about us here.
Blog

Blog

Know more about remote work.
Checkout our blog here.
Contact

Contact

Have any questions?
We'd love to hear from you.

Hire remote developers

Tell us the skills you need and we'll find the best developer for you in days, not weeks.

Hire Developers