GSD

Express.js Error Handling 101

Posted by Melvin Kosisochukwu on October 11, 2023

As software developers, we understand that building an entirely error-free application is impossible. In this article, we will look at handling errors in an Express application by building a sample Express REST API utilizing the ButterCMS Write API. Knowledge of JavaScript and the basics of Express applications are required. 

We have two types of errors in software development: operational and programmatic. Operational errors occur during runtime, and we must gracefully handle these exceptions to avoid the application shutting down abruptly. Program errors arise due to problems/bugs in the code. 

What is error handling?

Error handling is the procedure that checks and responds to bugs (abnormalities) in a program. Error handling provides helpful information on bugs in a program with messages containing the type of error that occurred and the stack/position where it occurred. 

Proper error handling in software development is paramount to its success and scalability. This relates to the fact that appropriate error handling makes it easier to resolve bugs during the production and development stages. In addition, error handling ensures that exceptions in an application are handled gracefully without halting execution unnecessarily.

Express.js error handling best practices

As with every task you set out to do with any piece of technology, there are a set of best practices you should know. Here are some general best practices you should follow when handling application errors:  

  • Send error logs: Error logs are essential when handling exceptions in an application. Error logs are sent to developers/clients and provide additional information on where an error occurred and, in some cases, how to fix the error.
  • Proper error messages: Another best practice is to provide error messages that appropriately describe the error that has occurred in an application.
  • Do not send error stack: You should not send the client the error stack (the location where an error occurred on the code base). Sending the error stack to the client poses a security risk to the server.
  • Shut down gracefully: Restart or shut down the application gracefully to handle uncaught exceptions in your code. This gives you the control to terminate the application abruptly and abort all ongoing or pending requests or close the server gracefully before shutting down the application.

Express.js specific best practices

These error handling practices help you properly handle errors with an Express server:

  • Catch all unhandled routes: This is where the application will catch all endpoints that do not exist in the application and respond with the appropriate error message.
  • Catch all uncaught and unhandled errors: We accomplish this by setting a node event listener for exceptions not caught by the compiler or unhandled rejections in an express application. We can effectively handle these errors when the events are triggered.
  • Set up error handling middleware for operational errors: In an Express application, it is essential to have all your error handlers in a central module; this is the function of the error handling middleware.
  • Set up a class that extends the Error class: This will handle operational errors in an Express application. Express.js cta banner

Project Setup

In this article, we will build a simple Express application that fetches, updates, and creates posts using the ButterCMS Write API and library.

The first process will be to create the project folder and initialize a node application, with server.js as the entry point, by running the command in our terminal:  

yarn init

Update the scripts in package.json and install the necessary packages:

...
  "scripts": {
...
    "start": "NODE_ENV=production node server.js",
    "start-dev": "NODE_ENV=development nodemon server.js",
    "start-prod": "NODE_ENV=production nodemon server.js"
  },
...
yarn add axios buttercms dotenv express nodemon

At this stage, we have bootstrapped the project. We will follow up by setting up an Express server. In the root directory, we will create an app.js and server.js file:

const Butter = require("buttercms");
const express = require("express");


const app = express();

app.use(express.json());
app.use(express.urlencoded({ extended: true, limit: "15kb" }));

const token = process.env.BUTTER_CMS_API_TOKEN;
const butter = token ? Butter(token) : null;

module.exports = app;

In the server.js file, we will set up the server to listen to port 4000:

const dotenv = require("dotenv");
// Set up environment variables configuration
var nodeEnvironment = process.env.NODE_ENV || "development";
dotenv.config({ path: `./${nodeEnvironment}.env` });

const app = require("./app");

const PORT = process.env.PORT || 4000;

const server = app.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}`);
});

In the server.js file, we are setting the configuration for the environment variables. First, we will assign the ButterCMS token to an environment variable.

The next step will be to set up our routes for requests. 

Getting all posts

Here we will be using the ButterCMS JavaScript client to fetch all items:

app.get(
  "/",
  catchAsync(async (req, res) => {
    const { data: posts } = await butter.post.list({ page_size: 10, page: req.query.page || 1 });

    res.status(200).json(posts);
  })
);

In the code block above, we have an Express GET method where we fetch all the posts on your ButterCMS dashboard by calling the butter.post.list() method. The list() method accepts the configuration for fetching the posts. Also, we have the catchAsync() function, which wraps up our asynchronous function. This will catch all errors in the async function. You can find the catchAsync() function here:  utils/catchAsync.js.

Getting a single post

To get a single post, we need to pass the slug to the post we want to fetch. We will get this from the Express route as a  parameter and pass it to the butter.posts.retrieve() method as an argument.

app.get(
  "/:slug",
  catchAsync(async (req, res) => {
    const { data: post } = await butter.post.retrieve(req.params.slug);

    res.status(200).json(post);
  })
);

Creating a new post 

To create a new post, we will need to make a post request to the ButterCMS API, and this is only possible with a Write access token from ButterCMS. You can get a Write token from ButterCMS by sending an email to support@buttercms.com.

const { default: axios } = require("axios");

module.exports = async ({ uri, method = "get", data }) => {
  method = ["get", "post", "put", "patch", "delete"].includes(method.toLocaleLowerCase()) ? method : "get";
  const response = await axios({
    url: "https://api.buttercms.com/v2/posts" + (uri || ""),
    method: method,
    data: data,
    headers: {
      Authorization: "Token " + process.env.BUTTER_CMS_API_TOKEN,
      "Content-Type": "application/json",
    },
  });
  return response.data;
};

The code block above is a utility function for making requests using the ButterCMS API. It accepts an object as an argument with a URI, the request method, and the data/payload for the request. We will pass the Write access token to “Authorization” in the request headers. Finally, the function will return the response data. We will be using the makeAxiosRequest() function to make write requests for creating and updating existing posts. 

The code block below creates a new post utilizing the ButterCMS Write API:

app.post(
  "/",
  catchAsync(async (req, res) => {
    let payload = req.body;

    await makeAxiosRequest({ method: "post", data: payload });

    res.status(201).json({
      status: "success",
      message: "Post created successfully",
    });
  })
);

Here, you can find additional information on creating a post with the ButterCMS API: https://buttercms.com/docs/api/?javascript#create-blog-post-via-write-api.

Updating a post

To update a post, we need to make a patch request to the ButterCMS API with the post slug passed as a URL parameter.

app.patch(
  "/:slug",
  catchAsync(async (req, res) => {
    let payload = {
      ...req.body,
    };

    await makeAxiosRequest({ method: "patch", data: payload, uri: `/${req.params.slug}/` });

    res.status(200).json({
      status: "success",
      message: "Post updated successfully",
    });
  })
);

NOTE: It is crucial to ensure the request URL ends with “/”.

You can find information on updating a blog post here.

After setting up the routes for the project, the next course of action will be to set up error handlers. However, we will need to create a new error class to handle operational errors before we do this. 

class AppError extends Error {
  constructor(message, statusCode) {
    super(message);
    this.statusCode = statusCode;
    this.status = `${statusCode}`.startsWith("4") ? "fail" : "error";
    this.isOperational = true;
    Error.captureStackTrace(this, this.constructor);
  }
}

module.exports = AppError;

In the code block above, we have an AppError class that extends the Error class. In the constructor, we have two arguments: message and statusCode, the message explains what error occurred and the statusCode is the value for the error response code

Global error middleware

The global error middleware is where we will be handling application errors. The global error middleware determines how the application will oversee production and development errors. For example, we do not want to send the client error messages with information on where the error occurred on the server—we will handle this in the global error middleware. At the project’s root, we will create a new folder called middleware and create the errorMiddleware.js file. 

const AppError = require("../utils/AppError");

...

// handles productional error
const productionError = (err, res) => {
  // operational error: send message to client about the error
  if (err.isOperational) {
    res.status(err.statusCode).json({
      status: err.status,
      message: err.message,
    });
  } else {
    // Sends a generic message to the client about the error
    res.status(500).json({
      status: "error",
      message: "Something went wrong",
    });
  }
};

// Handles development errore
// sends back the error message, and additional information about the error
const developmentError = (err, res) => {
  res.status(err.statusCode).json({
    status: err.status,
    message: err.message,
    error: err,
    stack: err.stack,
  });
};

// exports the function that handles the error
module.exports = (err, req, res, next) => {
  err.statusCode = err.statusCode || 500;
  err.status = err.status || "error";



  if (process.env.NODE_ENV === "development") {
    developmentError(err, res);
  }

  if (process.env.NODE_ENV === "production") {
    let error = { ...err };

    console.log("\n\n------ begin: ------");
    console.log("ERROR: ", error);
    console.log("------ end: ------\n\n");

...

    productionError(error, res);
  }
};

In the code block above, we have the developmentError() function, which will handle all errors during development. Here, we will send the complete error information and stack where the error occurs. The productionError() function handles sending errors during production. We will send production errors with information about the error to the client. We will send operational errors (errors recognized by the server) with a dynamic error message to the client and send a generic message for errors not recognized by the server to the client. The complete error message is logged on the console to aid in debugging. 

Handling database errors

The next action will be to set up database errors, which we will do in the error middleware. The first error we will be handling will be for invalid tokens. From the ButterCMS error documentation, we know that invalid tokens have status code 401. So, we check for an error response with status code 401 and return a new AppError.

...

// BCM = ButterCMS
const handleInvalidTokenBCMS = () => {
  const message = "The authorization token is invalid";
  return new AppError(message, 401);
};

...

// exports the function that handles the error
module.exports = (err, req, res, next) => {
  err.statusCode = err.statusCode || 500;
  err.status = err.status || "error";

  if (process.env.NODE_ENV === "development") {
    developmentError(err, res);
  }

  if (process.env.NODE_ENV === "production") {
    let error = { ...err };

    console.log("\n\n------ begin: ------");
    console.log("ERROR: ", error);
    console.log("------ end: ------\n\n");

    if (error?.response?.status === 401) {
      error = handleInvalidTokenBCMS();
    }

...

    productionError(error, res);
  }
};

We will also be handling other errors from ButterCMS: invalid slug, missing request resource, and error for author(s) not added to the CMS. 

const AppError = require("../utils/AppError");

// BCM = ButterCMS
const handleInvalidTokenBCMS = () => {
  const message = "The authorization token is invalid";
  return new AppError(message, 401);
};

const handleNotFoundErrors = () => {
  const message = "The requested resource could not be found";
  return new AppError(message, 404);
};

const handleAuthorErrorBCMS = (authorsArr = []) => {
  const authors = authorsArr.map((author) => author.split(" ")[0]).join(", ");
  const message = `The author${authors.includes(",") ? "s" : ""} (${authors}) do not exist on your CMS`;
  return new AppError(message, 400);
};

const handleInvalidRequest = () => {
  const message = "The request is invalid";
  return new AppError(message, 400);
};

const handleExistingSlug = (slug) => {
  const message = slug[0];
  return new AppError(message, 400);
};

...

    if (error?.response?.status === 401) {
      error = handleInvalidTokenBCMS();
    }

    if (error?.response?.status === 400 && error?.response?.data?.hasOwnProperty("author")) {
      error = handleAuthorErrorBCMS(error.response.data.author);
    }
    if (error?.response?.status === 400 && error?.response?.data?.hasOwnProperty("slug")) {
      error = handleExistingSlug(error.response.data.slug);
    }

    if (error?.response?.status === 404) {
      error = handleNotFoundErrors();
    }

    if (error?.response?.status === 400) {
      error = handleInvalidRequest();
    }

    productionError(error, res);
  }
};

After checking for an invalid token, we check for an unregistered author error. First, we check if the error has status code 400 and that the ButterCMS error data has the author property. If so, we will call the handleAuthorErrorBCMS() function which accepts error.response.data.author as an argument. 

Next, for an invalid post slug, we will check if the response data has the key slug. If the conditions are true, we will call the handleExistingSlug() function with error.response.data.slug passed as an argument. Subsequently, we handle 404 errors for resources not found and 400 errors for invalid request parameters. The placement for the error handlers is important as the invalid slug and author errors have a status of 400.

After setting up the error middleware, we will pass it into the Express app in the app.js file:

...
// Global error handler
app.use(errorMiddleware);

module.exports = app;

Express.js cta banner image

Unhandled routes

To set up error handling for routes that do not exist on the server, we will have an app.all route with a wild card (*) as the path. We will place this after all the other routes: 

// Wrong path handler
app.all("*", (req, _, next) => {
  next(new AppError(`Path ${req.originalUrl} does not exist for ${req.method} method`, 404));
});

The code block above will catch all requests that do not match a route on the server. We will return a new AppError with statusCode 404.

Unhandled rejections and uncaught exceptions

Sometimes, there are errors that the error middleware won’t catch. We will handle these errors with node events for catching uncaught exceptions and unhandled rejections.

const dotenv = require("dotenv");
// Set up environment variables configuration
var nodeEnvironment = process.env.NODE_ENV || "development";
dotenv.config({ path: `./${nodeEnvironment}.env` });

const app = require("./app");

const PORT = process.env.PORT || 4000;

process.on("uncaughtException", (err) => {
  console.log("Uncaught Exception: ", err.message);
  console.log("Closing server now...");
  process.exit(1);
});

const server = app.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}`);
});

process.on("unhandledRejection", (err) => {
  console.log(err);
  console.log("Closing server now...");
  server.close(() => {
    process.exit(1);
  });
});

process.on("SIGTERM", () => {
  console.log("SIGTERM received. Shutting down gracefully");
  server.close(() => {
    console.log("Closed out remaining connections");
    process.exit(0);
  });
});

From the code above, we have an event that catches uncaught exceptions in the code. When this happens, we will console log the error and shut down the app. For the unhandledRejection event, we will log the error and close the server before exiting the application. process.exit(1) will exit the application with a failure. We also have a node event for SIGTERM. This event shuts down or terminates the application. process.exit(0) will exit the application without a failure.

Closing thoughts

Error handling in an application is necessary—it determines if you can fix bugs in a couple of minutes or in a couple of hours. Therefore, developers should prioritize error handling in all applications which will ease the application's development, debugging, and ability to scale. 

In this article, we saw how we could effectively handle errors in an Express application to reduce debugging time, log errors in development to ease bug fixes, and send appropriate error messages in both development and production. We also covered how to handle operational errors for ButterCMS and provided adequate information on what error occurred and where the error occurred. 

You can check out the official documentation for additional information on how to use ButterCMS. In addition, you can find the complete code for this project in this Github Repository and the postman documentation for the API here

You can try building a blog site served from an express app, leveraging the Express.js error handling practices reviewed in this article.

Make sure you receive the freshest Express.js tutorials and Butter product updates.
    
Melvin Kosisochukwu

Melvin Kosisochukwu is a Software Developer/Technical writer, whose interests are focused on blockchain, SaaS, data, and creating content and tutorials that ease the onboarding process into the software development space for beginners and enthusiasts alike.

ButterCMS is the #1 rated Headless CMS

G2 crowd review award G2 crowd review award G2 crowd review award G2 crowd review award G2 crowd review award G2 crowd review award G2 crowd review award G2 crowd review award G2 crowd review award G2 crowd review award G2 crowd review award G2 crowd review award

Don’t miss a single post

Get our latest articles, stay updated!