What is an Error Handler in Express.js?
Error handlers in Express.js are in charge of catching and handing errors that happen during runtime. By default, Express.js provides with a built-in by default.
How Does The Error Handler Work in Express.js?
Whenever there is an error occurring in the server, unless you have a custom error handler, Express.js catches the error and sends a response to the client with the error message using its default error handler. The reason Express.js is eager to catch all errors is not only handling properly the errors, but also to correctly empty out unused resources after the application starts running again.
In other words, Express.js is preventing the server from having memory leaks and properly “garbage collects” objects once an API endpoint request is handled by a function.
Let’s explain using an example. Let’s say we have attached a handler function a simple API endpoint.
app.get('/car/model', function (req: express.Request, res: express.Response) {
const car = { model: 'Santa Fe', year: 2010, brand: 'Hyundai' };
res.send(`The car model is ${car.model}`); });
Besides the fact of returning a message with car model, let’s dig a little deeper of what is going on.
We created a car variable which contains a car object. We are allocating memory space every time we create a variable. As soon as we send a response back to the client, we are no longer needing the car variable. However, in theory car variable still uses memory space.
Does it mean we are loading up the memory every time we send a response to the client?
When incorrectly handled, yes. We reduce the memory space by not correctly freeing up that memory space. In other words, this is what we call memory leaks.
But! Express.js is protecting us from suffering memory leak issues by using a default error handler.
What Does the Error Handler Have to Do with Preventing from Suffering Memory Leaks?
In order to understand how error handlers are helping from Express.js application from crashing, we need to first understand how the Node.js engine works.
If you didn’t know, Node.js uses V8 engine. V8 engines provides with a built-in garbage collector to manage memory space which leads into a better performance and takes away that extra work from developers.
If you want to understand how the garbage collection work using the V8 engine in depth, I recommend you to read the article written on the V8 engine website. In short, the V8 engine determines memory as dead or alive, meaning if a memory spaces is used, then it is alive. If it is not longer used, then it is dead. Those with the dead status are eligible for garbage collection.
If we go back to the example of the API endpoint using a simple handler function, you will notice that the function is scoped. Once we send a response to the client, the function and any scoped variables are no longer used, making them eligible under the V8’s garbage collection engine.
In the case we force an error in the handler function, such as:
app.get('/car/model', function (req: express.Request, res: express.Response) {
const car = { model: 'Santa Fe', year: 2010, brand: 'Hyundai' };
// forcing to trigger an error
car.model();
res.send(`The car model is ${car.model}`);
});
And there were no error handlers, the client would never get a response in first place. Also, the request will “hang” on, meaning that our handler function would be using alive memory. Since this memory is alive, the V8 engine garbage collection would not work, causing memory leaks in the server.
Are There Problems with Using Express.js Default Error Handler?
There are no problems with the default error handler. However, it is important for you to understand some concepts. Let’s say we use our previous code example to manually force to trigger an error. I encourage you to make a request to the endpoint. You should get an error response, which it is expected.
However, this error response is not user friendly. It is rather technical and provides with the stack information about the server, such as the folder structure of the API. This information is beneficial if you are troubleshooting errors during development stages, but dangerous if this were to happen in production environments.
Fortunately, Express.js can prevent displaying any sensitive information from errors in the production stages. To make this work, you should make sure the NODE_ENV var is set to “production” prior to creating the express application.
Why Should I Implement a Custom Error Handler?
There can be different reasons why you would want to implement a custom error handler. For example, there projects that don’t set NODE_ENV as “production” in production stages. This could lead to leaking sensitive information about the system if not properly handled.
There are other projects that require to send a specific error object format whenever some unexpected error occurs.
How to Write a Custom Error Handler in Express.js using TypeScript?
Error handler middleware work very similar to any other middleware. However, there are a few things to take into consideration.
When developing an application-level or a route-level middleware, the handler function expects to receive three arguments: Request , Response , and NextFunction. In the case of developing a custom handler error, the handler function is expected to have four arguments. ErrorRequestHandler , Request , Response , and NextFunction.
Let’s go ahead an create our own custom error handler. In our case, we want to send custom error messages in case we want to provide meaningful information to the client about why the server cannot execute a certain process. One example could be of reading the records of a spreadsheet send by the client containing invalid data type for a specific column. Therefore, our first step is to create a CustomError object.
1. Create CustomError Object
First, create a custom-error.model.ts file in the following folder location: src/api/models. Once it is created, we are going to generate a class and export it.
export class CustomError {
message!: string;
status!: number;
additionalInfo!: any;
constructor(message: string, status: number = 500, additionalInfo: any = {}) {
this.message = message;
this.status = status;
this.additionalInfo = additionalInfo
}
}
2. Create Custom Error Handler Middleware
Create a file called error-handler.middleware.ts inside the src/api/middlewares folder. Once created, we are going to add the following logic:
import { Request, Response, NextFunction } from 'express';
import { CustomError } from './../models/custom-error.model';
/**
* Custom error handler to standardize error objects returned to
* the client
*
* @param err Error caught by Express.js
* @param req Request object provided by Express
* @param res Response object provided by Express
* @param next NextFunction function provided by Express
*/
function handleError(
err: TypeError | CustomError,
req: Request,
res: Response,
next: NextFunction
) {
let customError = err;
if (!(err instanceof CustomError)) {
customError = new CustomError(
'Oh no, this is embarrasing. We are having troubles my friend'
);
}
// we are not using the next function to prvent from triggering
// the default error-handler. However, make sure you are sending a
// response to client to prevent memory leaks in case you decide to
// NOT use, like in this example, the NextFunction .i.e., next(new Error())
res.status((customError as CustomError).status).send(customError);
};
export default handleError;
3. Attach Custom Error Handler as The Last Middleware to Use
Open the index.ts file. Then, import the custom handler and add it to the application as the last middleware. In other words, make sure to use the last app.use for the error handler middleware. If you are following the Learn How to Use TypeScript With Node.js and Express.js series, your index.ts file should look like the following:
import express from 'express';
import compression from 'compression';
import helmet from 'helmet';
import bodyParser from 'body-parser';
import cors from 'cors';
import { generateToken } from './api/utils/jwt.utils';
import routes from './api/routes';
import logger from './api/middlewares/logger.middleware';
import errorHandler from './api/middlewares/error-handler.middleware';
const app = express();
const port = 3000;
// Only generate a token for lower level environments
if (process.env.NODE_ENV !== 'production') {
console.log('JWT', generateToken());
}
// compresses all the responses
app.use(compression());
// adding set of security middlewares
app.use(helmet());
// parse incoming request body and append data to `req.body`
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
// enable all CORS request
app.use(cors());
// add logger middleware
app.use(logger);
app.use('/api/', routes);
// add custom error handler middleware as the last middleware
app.use(errorHandler);
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`)
});
4. Test Custom Handler
First, force an error in one of the function handlers for any of your API endpoints. In our case, we are going to modify the getTeams function we have in the teams.controller.ts file.
export const getTeams = (req: Request, res: Response) => {
const test = {};
//@ts-ignore
test.nonExistingMethod();
res.send(TEAMS);
};
Once, me make a request to PORT/api/teams/ we should get the following error object:
{
"message": "Oh no, this is embarrasing. We are having troubles my friend",
"status": 500,
"additionalInfo": {}
}
Now, let’s use the CustomError object and throw a CustomError instance inside our function handler instead.
import { Request, Response } from 'express';
import { CustomError } from './../models/custom-error.model';
const TEAMS = [
{ id: 1, name: 'Real Madrid', league: 'La Liga' },
{ id: 2, name: 'Barcelona', league: 'La Liga' },
{ id: 3, name: 'Manchester United', league: 'Premier League' },
{ id: 4, name: 'Liverpool', league: 'Premier League' },
{ id: 5, name: 'Arsenal', league: 'Premier League' },
{ id: 6, name: 'Inter', league: 'Serie A' },
{ id: 7, name: 'Milan', league: 'Serie A' },
{ id: 8, name: 'Juventus', league: 'Serie A' },
];
export const getTeams = (req: Request, res: Response) => {
throw new CustomError('forgot something?', 400, 'you can do better than that');
res.send(TEAMS);
};
The expected error object result should be the following:
{
"message": "forgot something?",
"status": 400,
"additionalInfo": "you can do better than that"
}
Top comments (0)