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.
