Simplifying Error Handling in Express Controllers: Introducing catchAsync Utility Function

Md Enayetur Rahman - Jun 13 - - Dev Community

Introduction

  • In any robust Express application, error handling is a critical aspect of maintaining reliability and user experience. Traditionally, writing controller functions involved wrapping asynchronous operations in try-catch blocks to ensure errors were properly caught and handled. However, this approach often led to repetitive boilerplate code across multiple controllers.

  • This is the eighth blog of my series where I am writing how to write code for an industry-grade project so that you can manage and scale the project.

  • The first seven blogs of the series were about "How to set up eslint and prettier in an express and typescript project", "Folder structure in an industry-standard project", "How to create API in an industry-standard app", "Setting up global error handler using next function provided by express", "How to handle not found route in express app", "Creating a Custom Send Response Utility Function in Express" and "How to Set Up Routes in an Express App: A Step-by-Step Guide". You can check them in the following link.

https://dev.to/md_enayeturrahman_2560e3/how-to-set-up-eslint-and-prettier-1nk6

https://dev.to/md_enayeturrahman_2560e3/folder-structure-in-an-industry-standard-project-271b

https://dev.to/md_enayeturrahman_2560e3/how-to-create-api-in-an-industry-standard-app-44ck

https://dev.to/md_enayeturrahman_2560e3/setting-up-global-error-handler-using-next-function-provided-by-express-96c

https://dev.to/md_enayeturrahman_2560e3/how-to-handle-not-found-route-in-express-app-1d26

https://dev.to/md_enayeturrahman_2560e3/creating-a-custom-send-response-utility-function-in-express-2fg9

https://dev.to/md_enayeturrahman_2560e3/how-to-set-up-routes-in-an-express-app-a-step-by-step-guide-177j

Traditional Approach to Writing Controllers

import httpStatus from 'http-status';
import { NextFunction, Request, Response } from 'express';
import sendResponse from '../../utils/sendResponse';
import { UserServices } from './user.service';

const createStudent = async (
  req: Request,
  res: Response,
  next: NextFunction,
) => {
  try {
    const { password, student: studentData } = req.body;

    // Call to service layer to create student in the database
    const result = await UserServices.createStudentIntoDB(
      password,
      studentData,
    );

    // Send success response
    sendResponse(res, {
      statusCode: httpStatus.OK,
      success: true,
      message: 'Student is created successfully',
      data: result,
    });
  } catch (err) {
    // Pass error to global error handler
    next(err);
  }
};

export const UserControllers = {
  createStudent,
};

Enter fullscreen mode Exit fullscreen mode

Explanation:

  • createStudent Function: This function handles the creation of a student entity in a database. It expects parameters req (request), res (response), and next (next middleware function).

  • try-catch Block: Wraps the asynchronous operation (await UserServices.createStudentIntoDB) to catch any errors that might occur during database interaction.

  • Sending Response: Upon successful creation, it sends a JSON response using sendResponse utility function with status code 200 (OK), indicating success, a message, and the data returned from the service layer.

  • Error Handling: If an error occurs during the database operation, it forwards the error (err) to the next middleware function (next(err)), typically the global error handler.

catchAsync Utility Function

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

const catchAsync = (fn: RequestHandler) => {
  return (req: Request, res: Response, next: NextFunction) => {
    Promise.resolve(fn(req, res, next)).catch((err) => next(err));
  };
};

export default catchAsync;

Enter fullscreen mode Exit fullscreen mode

Explanation:

  • catchAsync Function: This utility function accepts a request handler function (fn: RequestHandler) as its parameter and returns a new function that handles asynchronous operations.

  • Async Error Handling: Inside the returned function, it wraps the invocation of fn(req, res, next) in a Promise.resolve() to ensure it always returns a promise.

  • Error Propagation: If the promise resolves successfully, the response is passed to the next middleware. If it rejects (throws an error), next(err) is called to propagate the error to the global error handler.

Controller Using catchAsync Utility Function

import httpStatus from 'http-status';
import catchAsync from '../../utils/catchAsync';
import sendResponse from '../../utils/sendResponse';
import { UserServices } from './user.service';

const createStudent = catchAsync(async (req, res) => {
  const { password, student: studentData } = req.body;

  // Call to service layer to create student in the database
  const result = await UserServices.createStudentIntoDB(password, studentData);

  // Send success response
  sendResponse(res, {
    statusCode: httpStatus.OK,
    success: true,
    message: 'Student is created successfully',
    data: result,
  });
});

export const UserControllers = {
  createStudent,
};

Enter fullscreen mode Exit fullscreen mode

Explanation:

  • Usage of catchAsync: Instead of manually wrapping the controller function (createStudent) in a try-catch block, we use catchAsync to handle asynchronous operations and error handling.

  • Simplified Error Handling: This approach eliminates the need for explicit try-catch blocks in each controller function, reducing boilerplate code and ensuring consistent error handling across the application.

  • Send Response: Once the database operation completes successfully, it sends a JSON response using sendResponse with status code 200, a success message, and the data returned from the service.

Benefits of Using catchAsync

  • Code Clarity: Promotes cleaner and more readable code by abstracting error-handling logic into a reusable utility function.

  • Consistent Error Handling: Ensures that errors are handled uniformly across all controller functions, enhancing maintainability.

  • Enhanced Developer Productivity: Reduces the amount of repetitive code, allowing developers to focus more on business logic rather than error-handling boilerplate.

Conclusion

Implementing catchAsync in an Express application streamlines error management and improves code quality, making it a valuable tool for developers building scalable and maintainable APIs. This approach not only simplifies error handling but also improves overall code organization and developer productivity.

. . . . . . . . . . . . . . . . . . .