Regardless of the language being used or the project being developed, we all need to deal with errors at some point. And if we don't do something about it, when we get to production we may have several problems related to them.
I have already noticed that in the JavaScript community it is not very common to see a tutorial, whether in video or written format, explaining what problems we may have and why we have to deal with errors.
So when we use frameworks that give us complete freedom (like Express.js) it's even rarer, but for example I believe that a NestJS developer must have used Exception Filters at least once.
Types of Errors
In my opinion there are only two types of errors, one of them as you may be thinking is human error and this error can happen for many reasons. Perhaps the most popular are completely ignoring error handling or simply doing it incorrectly, which leads to unexpected errors and bugs.
The second type of error is when one occurs during runtime, for example when the API was not well designed, with bad code or with a library that was not very well chosen, which would result in memory leaks everywhere.
I know these points seem superficial, but not all the problems we face on a daily basis are related to the technologies we are using, but the way we deal with them.
Overview
The ideal approach would be to create a Custom Error Handler, but for beginners it can be more confusing than helpful. In this case I decided to explain how errors are handled in Express.js and then we'll move on to the code.
First of all, Express already has a built-in error handler ready to be used by developers, which can be used either in synchronous or asynchronous routes.
The problem is that Express doesn't handle asynchronous routes very well, in this case we have two options, either we use a dependency that solves our problem, or we could create a function that makes all our routes asynchronous. For today's example, we're going to use a dependency that makes all this simple.
Express middleware works like a Stack and it's worth noting that the order in which the middlewares are defined matters.
Imagine you have the following code:
const express = require('express')
const cors = require('cors')
const someMiddleware = (req, res, next) => {
console.log("Hello from the middleware")
next()
}
const startServer = () => {
const app = express()
app.use(cors())
app.get('/', someMiddleware, (req, res) => {
res.send('Hello World!')
})
app.listen(3000)
}
startServer()
Viewed from a high level perspective, once the request was made on the /
route, the stack would look like the following image:
First Express would receive the http request, which would then go through our first middleware, which is cors, then it would go to our router, which in turn would handle the /
route. First we would go through someMiddleware and finally we would go to our controller that would have the logic.
And ideally we would handle the error, either in the middleware or in the controller. But what matters is that the error handler is defined last, so that the error is always handled at the end of the stack.
Let's code
As always, first let's install our dependencies:
# NPM
npm install express express-async-errors
# YARN
yarn add express express-async-errors
# PNPM
pnpm add express express-async-errors
Then let's create our base:
import "express-async-errors";
import express from "express";
const startServer = async () => {
const app = express();
app.use(express.json());
app.get("/", (req, res) => {
return res.json({ message: "Hello World 🌵" });
});
return app;
};
startServer()
.then((app) => app.listen(3000))
.catch((err) => console.log(err));
Now we can define our error handler middleware:
import "express-async-errors";
import express from "express";
const errorHandler = (err, req, res, next) => {
// ...
};
const startServer = async () => {
// hidden for simplicity
};
startServer()
.then((app) => app.listen(3000))
.catch((err) => console.log(err));
As you may have noticed, unlike controllers and "normal" middlewares, the error middleware has four arguments, and the difference is the error argument.
First of all, it would be ideal to create a simple log, which wanted the path of the http request, the http verb used and the error definition. Something like this:
import "express-async-errors";
import express from "express";
const errorHandler = (err, req, res, next) => {
console.log(
`[Error Handler]: Path: ${req.path}, Method: ${req.method}, ${err.stack}`
);
// ...
};
const startServer = async () => {
// hidden for simplicity
};
startServer()
.then((app) => app.listen(3000))
.catch((err) => console.log(err));
Then we'll do what you're used to, we'll use res
to set the status code of the response and the body content of the response will be a json with only the message property.
import "express-async-errors";
import express from "express";
const errorHandler = (err, req, res, next) => {
console.log(
`[Error Handler]: Path: ${req.path}, Method: ${req.method}, ${err.stack}`
);
return res.status(err.status || 500).json({
message: err.message,
});
};
const startServer = async () => {
// hidden for simplicity
};
startServer()
.then((app) => app.listen(3000))
.catch((err) => console.log(err));
With our error handler created, we can add it to the stack like so:
import "express-async-errors";
import express from "express";
const errorHandler = (err, req, res, next) => {
// hidden for simplicity
};
const startServer = async () => {
const app = express();
app.use(express.json());
app.get("/", (req, res) => {
return res.json({ message: "Hello World 🌵" });
});
app.use(errorHandler);
return app;
};
startServer()
.then((app) => app.listen(3000))
.catch((err) => console.log(err));
Finally, let's create a route with the /say-hi
endpoint, in which we will send a json with the username property in the response body. If the username is not sent we will throw the error, otherwise we will send the username "formatted" in a string. This way:
import "express-async-errors";
import express from "express";
const errorHandler = (err, req, res, next) => {
console.log(
`[Error Handler]: Path: ${req.path}, Method: ${req.method}, ${err.stack}`
);
return res.status(err.status || 500).json({
message: err.message,
});
};
const startServer = async () => {
const app = express();
app.use(express.json());
app.get("/", (req, res) => {
return res.json({ message: "Hello World 🌵" });
});
app.post("/say-hi", (req, res) => {
const { username } = req.body;
if (!username) throw new Error("Username is required");
return res.json({ message: `Hello ${username}! 👋` });
});
app.use(errorHandler);
return app;
};
startServer()
.then((app) => app.listen(3000))
.catch((err) => console.log(err));
Now, whenever we want to handle the error, just use throw new Error
with a message to make it easier to debug. And one of the advantages we have is that in this way the handling of errors was centralized.
The End
I hope the article was useful, I tried to simplify it as much as possible so that it is informative and that I can get a visual idea from here as well as a code snippet to use. See you 👊
Top comments (0)