Logo Sujal Magar
Custom Express Async Handler

Custom Express Async Handler

October 1, 2024
3 min read
Table of Contents

Working with try catch everytime can be tedious and repetitive. We can use npm packages like express-async-handler but why bother installing third party packages when we can make our own custom express async handler.

1. Example

1.1. Custom express async handler (with Typescript)

Create a async-handler.ts file and paste the code below.

import { Request, Response } from 'express'
 
interface ErrorSchema {
  // define your error schema here
}
 
const handleError = (res: Response, error: ErrorSchema) => {
  // handle your error here
}
 
export const asyncHandler =
  (fn: Function) => (req: Request, res: Response, next: Function) => {
    Promise.resolve(fn(req, res, next)).catch((error) =>
      handleError(res, error),
    )
  }

2. Explanation

Higher-order function:

  • The asyncHandler is a function that takes another function (fn) as an argument and returns a new function.
  • This new function will act as middleware in an Express route handler, expecting req, res, and next as parameters, which are typical for Express handlers.

fn argument:

  • fn is a function that represents an asynchronous operation (like a route handler).
  • It’s expected to return a Promise. This could be anything like interacting with a database, making HTTP requests, or any asynchronous task within an Express route.

Executing the fn function:

  • Inside the returned function, Promise.resolve(fn(req, res, next)) ensures that whatever fn returns is converted into a resolved or rejected promise.
  • Even if fn is not explicitly returning a promise (e.g., a synchronous function), this ensures a consistent promise-based behavior.

Error handling:

  • If fn throws an error or its returned promise rejects, .catch() will capture it.
  • The catch block then invokes handleError(res, error), passing the error to a centralized error-handling function handleError (presumably defined elsewhere in the application). This prevents the need for writing repetitive try-catch blocks in each route handler.

3. Use case example

3.1. Example of a basic try catch statement

const getAllProducts = async (req: Request, res: Response) => {
  try {
    const product = await ProductModel.find({}).limit(20)
 
    const productResponse: TGetAllProducts = {
      status: 200,
      data: product,
      success: true,
      message: 'Fetched all the products successfully',
    }
 
    res.status(200).json(productResponse)
  } catch (error) {
    handleError(res, error)
  }
}

3.2. Same example as above but without try catch statement

const getAllProducts = asyncHandler(async (req: Request, res: Response) => {
  const product = await ProductModel.find({}).limit(20)
 
  const productResponse: TGetAllProducts = {
    status: 200,
    data: product,
    success: true,
    message: 'Fetched all the products successfully',
  }
 
  res.status(200).json(productResponse)
})

Above is a basic example, but working with multiple controller functions can cause you to repeat the same try catch statement for all the controllers which can be repetitive and tedious. Defining a custom async handler function helps tackle such problem and it also preserves the DRY principle of software development.