Try-Catch Pollution: Understanding the Problem and How to Fix It!

Try Catch Pollution and how to fix it
Try Catch Pollution and how to fix it

In the world of web development, clean and maintainable code is essential for building robust applications. In Express.js, one common issue developers face is "try-catch pollution," where repetitive try-catch blocks clutter the codebase, especially when handling errors in asynchronous functions. This not only leads to code duplication but also makes it harder to manage error handling consistently across an application.

In this article, we'll explore what try-catch pollution is, why it occurs frequently in Express.js applications, and most importantly, how to eliminate it by leveraging centralized error-handling patterns and utilities. With the right approach, you can make your Express.js code cleaner, more efficient, and easier to maintain—all without compromising on robust error handling.

Bad Example: Using Try-Catch (Try-Catch Pollution)

In this example, each controller method has its own try-catch block, leading to repeated error handling logic across the application. This results in try-catch pollution, making the code harder to maintain, cluttered, and repetitive.


class JobController implements IJobController {
  listJobs = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
    try {
      const jobs = await JobService.listJobs();
      if (jobs.length === 0) {
        return next(new ApiError(StatusCodes.NOT_FOUND, 'Jobs not found'));
      }
      res.status(StatusCodes.OK).send({ jobs, count: jobs.length });
    } catch (error) {
      next(new ApiError(StatusCodes.INTERNAL_SERVER_ERROR, `Error Occurred: ${(error as Error).message}`));
    }
  };

  newJob = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
    try {
      const job = await JobService.createJob(req);
      res.status(StatusCodes.CREATED).json({ job });
    } catch (error) {
      next(new ApiError(StatusCodes.INTERNAL_SERVER_ERROR, `Error Occurred: ${(error as Error).message}`));
    }
  };

  showJob = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
    try {
      const job = await JobService.showJob(req.params.id);
      if (!job) {
        return next(new ApiError(StatusCodes.NOT_FOUND, 'Job not found'));
      }
      res.status(StatusCodes.OK).json({ job });
    } catch (error) {
      next(new ApiError(StatusCodes.INTERNAL_SERVER_ERROR, `Error Occurred: ${(error as Error).message}`));
    }
  };
}

Issues with this Approach:

  • Repetitive Error Handling: Each method has its own try-catch block that performs similar tasks — catching the error and passing it to next().
  • Increased Complexity: As more methods are added, the same try-catch pattern is repeated, bloating the code.
  • Harder to Maintain: If you need to update the error-handling logic, you’ll need to modify every method separately.

Good Example: Without Try-Catch Pollution (Using Centralized Error Handling)

In this refactored example, the try-catch blocks have been eliminated. Errors are handled using Express’s built-in error-handling mechanism, where unhandled promise rejections are automatically forwarded to the error-handling middleware (ErrorHandler).


class JobController implements IJobController {
  listJobs = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
    try {
      const jobs = await JobService.listJobs();
      if (jobs.length === 0) {
        return next(new ApiError(StatusCodes.NOT_FOUND, 'Jobs not found'));
      }
      res.status(StatusCodes.OK).send({ jobs, count: jobs.length });
    } catch (error) {
      next(new ApiError(StatusCodes.INTERNAL_SERVER_ERROR, `Error Occurred: ${(error as Error).message}`));
    }
  };

  newJob = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
    try {
      const job = await JobService.createJob(req);
      res.status(StatusCodes.CREATED).json({ job });
    } catch (error) {
      next(new ApiError(StatusCodes.INTERNAL_SERVER_ERROR, `Error Occurred: ${(error as Error).message}`));
    }
  };

  showJob = async (req: Request, res: Response, next: NextFunction): Promise<void> => {
    try {
      const job = await JobService.showJob(req.params.id);
      if (!job) {
        return next(new ApiError(StatusCodes.NOT_FOUND, 'Job not found'));
      }
      res.status(StatusCodes.OK).json({ job });
    } catch (error) {
      next(new ApiError(StatusCodes.INTERNAL_SERVER_ERROR, `Error Occurred: ${(error as Error).message}`));
    }
  };
}

Centralized Error Handler (ErrorHandler.ts):

Here, the ErrorHandler middleware is responsible for catching any errors that occur during the request lifecycle. If any of the JobController methods throw an error or a rejected promise, they are automatically forwarded to this middleware.


import { NextFunction, Request, Response } from "express";
import { StatusCodes } from "http-status-codes";
import ApiError from "../helper/ApiError";
import logger from "../helper/logger";

class ErrorHandler {
  public static errorHandler(err: ApiError, req: Request, res: Response, next: NextFunction) {
    const { statusCode = StatusCodes.INTERNAL_SERVER_ERROR, message = "Internal Server Error", status = "error" } = err;
    logger.error(`${status.toUpperCase()} - Code: ${statusCode} - Message: ${message}`);

    if (err.name === 'ValidationError') {
      return res.status(StatusCodes.BAD_REQUEST).json({
        status: 'fail',
        message: err.message,
        details: err.errors,
      });
    }

    if (err.name === 'MongoNetworkError') {
      return res.status(StatusCodes.SERVICE_UNAVAILABLE).json({
        status: "error",
        message: "Database connection failed. Please try again later."
      });
    }

    res.status(statusCode).json({ status, message });
  }
}

export default ErrorHandler;

Benefits of This Approach:

  • Centralized Error Handling: The ErrorHandler middleware handles all errors in one place, making it easier to maintain and extend error handling logic.
  • Cleaner Controller Code: The controller methods are cleaner, focusing purely on business logic without the distraction of error handling.
  • Express’s Built-in Mechanism: By leveraging Express’s built-in error-handling mechanisms, rejected promises are caught automatically and passed to the middleware.

Conclusion

Eliminating try-catch pollution in Express.js is crucial for writing cleaner, more maintainable code. By relying on centralized error-handling middleware, you can ensure that errors are handled consistently across your application without cluttering your controller methods with repetitive try-catch blocks. This results in a cleaner codebase that is easier to manage and debug.

Full Code:  JOB API - GitHub repository: https://github.com/cholakovit/jobApi

Project Description Job API