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.
Table of contents
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.
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;
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.
ButterCMS is the #1 rated Headless CMS
Related articles
Don’t miss a single post
Get our latest articles, stay updated!
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.