This is going to be a short blog on handling error in Express using Typescript.
In this blog I will write about handle error gracefully in Express. Express is one of the most famous Node.js frameworks.
It’s crucial to distinguish between different types of errors in Node.js. Some errors are recoverable, while others are not. For instance, a wrong user input is a recoverable error, but a syntax error is not.
Accordingly Errors can be divided into two categories in Node.js:
- Operational errors (e.g. database connection error, invalid user input, etc.)
- Programming errors (e.g. syntax error, reference error, etc.)
Handling Programming Errors
Programming errors are usually not handled in Node.js. When they occur Node.js process is terminated. This is because programming errors are usually not recoverable and the process should be terminated to avoid further damage. We can listen to
unhandledRejectionand
uncaughtExceptionevents to handle programming errors. But it is generally recommended to just let the process crash and restart it using a process manager like pm2
1// unhandledRejection2process.on('uncaughtException', (err) => {3 // ... log the error or do any other stuff ...4 process.exit(1);5});67// un handled promise rejection8process.on('unhandledRejection', (err) => {9 // ... log the error ...10 // server.close will close the server and wait for all the connections to close11 server.close(() => {12 process.exit(1);13 });14});
Handling Operational Errors
For operational errors, we need to handle them gracefully. We need to send a proper response to the client and log the error for debugging purposes. For that, Let's look at middlewares first.
Express works with the concept of middleware. Middleware is a function that has access to the request and response object.
Middlewares can
- Modify the request and response object
- End the request-response cycle
- Call the next middleware in the stack
We also handle error in Express using middleware.
Let's learn all this by creating a simple Express app.
Create a simple Express app
First, we need to create a new directory and initialize a new node project.
1mkdir Express-error-handling2cd Express-error-handling34# generates package.json with default settings ( -y flag - yes to all questions)5npm init -y
Now we need to install Express and typescript.
1# install dependencies2npm i Express typescript ts-node34# install types for Express as dev dependency5npm i -D @types/Express @types/node
We are not going to explore more about configuring TypeScript and we are not going to create a tsconfig.json file. We are going to use ts-node to run our typescript code. next let's create a new file called
index.tsand add the following code.
1import Express from 'Express'23const app = Express()45app.get('/', (req, res) => {6 res.send('Hello World')7})89app.listen(3000, () => {10 console.log('Server is running on port 3000')11})
now that we have a simple Express app, let's add some error handling to it. We are going to create a new file called
error.tsand add the following code.
1// base error class2export class BaseError extends Error {3 status: number4 isOperational: boolean5 constructor(message: string, status: number, isOperational = true) {6 super(message)7 this.status = status8 this.isOperational = this.isOperational9 Object.setPrototypeOf(this, BaseError.prototype)1011 }12}
This will be the base class to other classes we are going to create to handle other type of errors. In this error class we have a constructor that takes three arguments.
- The first argument is the error message ( pretty standard way of indicating what happend)
- the second argument is the status code (This indicates what to return for the Rest API request)
- the third argument is a boolean value that indicates if the error is operational or not.
Let's add more error classes to our
error.tsfile.
1// 404 error class2class NotFoundError extends BaseError {3 constructor(message: string) {4 super(message, 404)5 Object.setPrototypeOf(this, NotFoundError.prototype)6 }7}89// validation error class10class ValidationError extends BaseError {11 errorData: Record<string, string>[]12 constructor(data: Record<string,string>[]) {13 super("Validation Error", 400)14 this.errorData = data15 Object.setPrototypeOf(this, ValidationError.prototype)16 }17}
we can create as many error classes as we want. In this example I have created two error classes. The first one is the
NotFoundErrorclass and the second one is the
ValidationErrorclass. next we need to create a middleware that will handle the errors. Error Handler Middleware is a middleware that takes four arguments. The first argument is the error object, the second argument is the request object, the third argument is the response object and the fourth argument is the next function. i.e If you don't provide 4 arguments to a middleware, Express will treat it as a normal middleware and not an error handler middleware.
we now create a new file called
errorHandler.tsand add the following code.
1// error handler middleware2 export const errorHandler = (err: Error, req: Request, res: Response, next: NextFunction) => {3 if(err instanceof ValidationError) {4 return res.status(err.status).json({5 status: 'fail',6 message: err.message,7 data: err.errorData8 })9 } else if (err instanceof BaseError) {10 if( err.isOperational) {11 return res.status(err.status).json({12 status: err.status < 500 && err.status >= 400 ? 'fail' :'error',13 message: err.message14 })15 //16 } else {17 // log the error18 console.log(err)19 // send generic error message20 return res.status(err.status).json({21 status: 'error',22 message: 'Something went wrong'23 })2425 }26 }27 }
now that we have created the error handler middleware, we need to add it to our Express app. We can do this by adding the middleware using
app.usemethod. let's add the following code to our
index.tsfile.
1app.use(errorHandler)
now we can use our error classes in our controllers and/or middleware to throw errors. let's add a new route to our
index.tsfile.
1// check if id is a number2const validateId = (req: Request, res: Response, next: NextFunction) => {3 const id = Number(req.params.id)4 if(isNaN(id)) {5 next(new ValidationError([6 {7 id: 'id needs to be a number :( '8 }9 ]))10 }11 next()12}1314app.get('/users/:id', validateId, (req, res) => {15 const id = req.params.id16 if(id === '1') {17 res.send('User 1')18 } else {19 next(new NotFoundError('User not found'))20 }21})
now we can test our app by running the following command.
1npx ts-node index.ts
Now when we hit the
/users/2route, we will get the following response.
1{2 "status": "fail",3 "message": "User not found"4}
with 404 status code.
and When we hit the route
/users/unknownwe will get the following response.
1{2 "status": "fail",3 "message": "Validation Error",4 "data": [5 {6 "id": "id needs to be a number :( "7 }8 ]9}
and We have successfully created a simple Express app with error handling.
For bigger projects and use-cases you can adapt the same strategy for error handling , even though you might have many and different error classes with different requirments for the error message and status code.
FYI , I have used Jsend to format our API response.
This is it for this blog. I hope you enjoyed it. If you have any questions, feel free to email me :)