Jun 9th 2024

Handling Errors in Express Typescript

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:

  1. Operational errors (e.g. database connection error, invalid user input, etc.)
  2. 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

unhandledRejection
and
uncaughtException
events 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// unhandledRejection
2process.on('uncaughtException', (err) => {
3 // ... log the error or do any other stuff ...
4 process.exit(1);
5});
6
7// un handled promise rejection
8process.on('unhandledRejection', (err) => {
9 // ... log the error ...
10 // server.close will close the server and wait for all the connections to close
11 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-handling
2cd Express-error-handling
3
4# 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 dependencies
2npm i Express typescript ts-node
3
4# install types for Express as dev dependency
5npm 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.ts
and add the following code.

1import Express from 'Express'
2
3const app = Express()
4
5app.get('/', (req, res) => {
6 res.send('Hello World')
7})
8
9app.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.ts
and add the following code.

1// base error class
2export class BaseError extends Error {
3 status: number
4 isOperational: boolean
5 constructor(message: string, status: number, isOperational = true) {
6 super(message)
7 this.status = status
8 this.isOperational = this.isOperational
9 Object.setPrototypeOf(this, BaseError.prototype)
10
11 }
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.ts
file.

1// 404 error class
2class NotFoundError extends BaseError {
3 constructor(message: string) {
4 super(message, 404)
5 Object.setPrototypeOf(this, NotFoundError.prototype)
6 }
7}
8
9// validation error class
10class ValidationError extends BaseError {
11 errorData: Record<string, string>[]
12 constructor(data: Record<string,string>[]) {
13 super("Validation Error", 400)
14 this.errorData = data
15 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

NotFoundError
class and the second one is the
ValidationError
class. 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.ts
and add the following code.

1// error handler middleware
2 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.errorData
8 })
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.message
14 })
15 //
16 } else {
17 // log the error
18 console.log(err)
19 // send generic error message
20 return res.status(err.status).json({
21 status: 'error',
22 message: 'Something went wrong'
23 })
24
25 }
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.use
method. let's add the following code to our
index.ts
file.

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.ts
file.

1// check if id is a number
2const 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}
13
14app.get('/users/:id', validateId, (req, res) => {
15 const id = req.params.id
16 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/2
route, 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/unknown
we 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 :)

Hello fellow developer 👋🏾


Looking for expert assistance with your web application development or code review?

Feel free to send me a message or email to discuss your specific needs