DEV Community

Arsalan Ahmed Yaldram
Arsalan Ahmed Yaldram

Posted on

Setup express with Typescript - global error handler and async middleware

Introduction

In this series we will setup an express server using Typescript, we will be using TypeOrm as our ORM for querying a PostgresSql Database, we will also use Jest and SuperTest for testing. The goal of this series is not to create a full-fledged node backend but to setup an express starter project using typescript which can be used as a starting point if you want to develop a node backend using express and typescript.

Overview

This series is not recommended for beginners some familiarity and experience working with nodejs, express, typescript and typeorm is expected. In this post which is part five of our series we will set up : -

  • Set up a global error handler.
  • Implement express async middleware library on our own.
  • Remove try / catch blocks from the controller.

Step One: Express Global Handler

In this section we will setup 2 handlers, one for handling all our global errors and the next for handling 404 not found routes.
Before doing so lets create some error classes. Under the src/utils folder create a new file called BaseError.ts -

export class BaseError extends Error {
  constructor(message: string) {
    super(message);
    this.name = this.constructor.name;
  }
}
Enter fullscreen mode Exit fullscreen mode

Similarly lets create another file under src/utils called NotFoundError.ts

import { BaseError } from './BaseError';

export class NotFoundError extends BaseError {
  status: number;

  constructor(message: string, status: number) {
    super(message);
    this.status = status;
  }
}
Enter fullscreen mode Exit fullscreen mode

If you are unfamiliar with creating custom error classes I would recommend you read - https://javascript.info/custom-errors.

Now under server.ts lets add the following 2 functions:

private globalNotFoundHandler() {
    this.app.use((req, res, next) => {
      const error = new NotFoundError('Resource not found', 404);
      next(error);
    });
  }

  private globalErrorHandler() {
    this.app.use(
      (error: Error, req: Request, res: Response, next: NextFunction) => {
        console.log('Error (Global Error Handler)', error.stack);
        if (error instanceof NotFoundError) {
          return res.status(error.status).json({
            status: false,
            statusCode: error.status,
            message: error.message,
          });
        }

        // Handling internal server errors
        return res.status(500).json({
          status: false,
          statusCode: 500,
          message: 'Something unusual Happened',
        });
      }
    );
  }
Enter fullscreen mode Exit fullscreen mode

In the above code we added a handler for handling 404 not found routes and a global error handler. In express if your middleware takes in 4 arguments namely error, req, res, next it is considered as an error handler middleware. You can see in the NotFoundError handler we used next(error) meaning we passed the error to our globalErrorHandler. We can also invoke our globalErrorHandler by throwing an error from our middlewares, controllers all throw error calls will land in globalErrorhandler.

Now lets call these handlers in our constructor like so -

 constructor() {
    // Initialize express application
    this.app = express();
    // add express middlewares
    this.addMiddlewares();
    // add our routes
    this.addRoutes();
    // handle 404 not found routes
    this.globalNotFoundHandler();
    // handle all global errors
    this.globalErrorHandler();
  }
Enter fullscreen mode Exit fullscreen mode

Make sure your 404 handler is called after your routes and global error handler is called last.

Step Two: Setup Async Handler Middleware

We will be using express-async-handler library for this. But instead on installing it, lets read their code on github, understand and implement it. Before that lets understand what a middleware is in express. Middlewares are like the layers of an onion in order to reach to the last layer you need to go through all the layers.

In the following code our last layer is our controller (which is the last piece of middleware). Before reaching the controller our request will go through all these middleware functions -

this.router.get(`/`, authMiddleware, validationMiddleware, todoController.createTodo)
Enter fullscreen mode Exit fullscreen mode

App wide our last layer (middleware) is our globalErrorhandler function.

Cool now how we write middlewares in express, a middleware is a function that receives req, res, next as arguments like -

const authMiddleware = (req, res, next) => {}
Enter fullscreen mode Exit fullscreen mode

To go from one layer to another layer express uses the next() function.

Now we all are familiar with currying in javascript. A curried function is a function that returns another function -

const curriedFunction = () => () => {}
Enter fullscreen mode Exit fullscreen mode

Now if I had to write my middleware in this fashion it would be -

const curriedMiddleware = () => (req, res, next) => {}
Enter fullscreen mode Exit fullscreen mode

And if I had to use this curriedMiddleware I will use it like so -

this.router.get(`/`, authMiddleware(), todoController.createTodo)
Enter fullscreen mode Exit fullscreen mode

This is the same pattern that express-async-middleware library uses. With this pattern : -

  • We can pass our controller function to the async middleware.
  • The async middleware will resolve our controller function catch any errors and if we get any error it will call next(error).
  • next(error) will end up in our globalErrorHandler and will gracefully handle all the internal server errors.

Now in our src folder create a new folder middlewares under it create a new file called asyncHandler.ts -

import { Request, Response, NextFunction } from 'express';

export function asyncHandler(controllerFunction: any) {
  return function asyncMiddleware(
    req: Request,
    res: Response,
    next: NextFunction
  ) {
    const controllerPromise = controllerFunction(req, res, next);
    return Promise.resolve(controllerPromise).catch(next);
  };
}
Enter fullscreen mode Exit fullscreen mode

This is the same code as the express-async-handler library:

  • First we have our curried function asyncHandler that accepts our controller function as an argument and returns the middleware function asyncMiddleware.
  • In our middleware function we get req, res, next arguments. Then we pass the req, res, next to our controller function after all we use them in our controllers.
  • const controllerPromise = controllerFunction(req, res, next); here we are executing our controller function, which will return a promise if our controller is async or will just return whatever our controller returns.
  • Finally we resolve our controllerPromise variable and if we catch any error we just call next(error) and this lands in our globalErrorHandler.

Step 3: Using async handler

Now lets use our async handler in our todos.router.ts we do -

 addRoutes(): void {
    this.router.get('/', asyncHandler(todosController.getAllTodos));
    this.router.post('/', asyncHandler(todosController.createTodo));
    this.router.get('/:todoId', asyncHandler(todosController.getTodoById));
  }
Enter fullscreen mode Exit fullscreen mode

We will pass our controller to the asyncHandler curried middleware. Lets remove all the try-catch blocks from our controller functions -

import { Request, Response } from 'express';

import { todosService } from './todos.service';

class TodoController {
  async getAllTodos(req: Request, res: Response) {
    const todos = await todosService.getAllTodos();

    return res.status(200).json({
      status: true,
      statusCode: 200,
      todos,
    });
  }

  async getTodoById(req: Request, res: Response) {
    const { todoId } = req.params;
    const todo = await todosService.getTodoById(todoId);
    if (!todo) {
      return res.status(404).json({
        status: false,
        statusCode: 404,
        message: `todo not found for id - ${todoId}`,
      });
    }

    return res.status(200).json({
      status: true,
      statusCode: 200,
      todo,
    });
  }

  async createTodo(req: Request, res: Response) {
    const { text, status } = req.body;
    const newTodo = await todosService.createTodo(text, status);

    return res.status(201).json({
      status: true,
      statusCode: 201,
      todo: newTodo.raw,
    });
  }
}

export const todosController = new TodoController();
Enter fullscreen mode Exit fullscreen mode

Look how lean and clean our controller functions are now. Lets test our endpoints whether everything is working fine by running npm run dev. To test the 404 not found handler visit a route which does not exists on your server. To test the global error Handler pass a json request which does not have the text field to create todo endpoint, TodoEntity will throw an error.

Overview

In this tutorial instead of pulling in the library we implemented it on our own by reading its github code. I encourage all the developers to read open source code it is very easy, it requires some time and attention but very helpful. All the code for this tutorial can be found under the feat/async-handler branch here. In the next tutorial we will setup request validation using zod and implement another library on our own until next time PEACE.

Top comments (0)