Centralized Error Handling

 My Experience with Error Handling and How Centralized Error Handling Helped Me

“Hi everyone! Before we dive into today’s topic, I want to take you back to a problem I encountered in my early days as a developer. Like many of you, I started building simple applications — small projects here and there. At first, I didn’t think much about error handling. I was so focused on getting things to work that I didn’t realize how important it was to have a good strategy for managing errors.

But as my project grew, I realized I had a problem. I had created multiple routes and API endpoints, each performing different tasks. There were tasks like user registration, login, data fetching, and updating user information. And every time something went wrong — a user didn’t exist, data couldn’t be fetched, or something failed on the backend — I had to handle those errors.

I started out handling each error directly inside the routes. It worked, but things got messy quickly. I had to check for errors in every single route. And as I added more features, I realized that my error-handling code was becoming repetitive, inconsistent, and difficult to maintain.

Imagine, you’ve got 10 routes, and each one handles its own errors. If you need to change how you handle errors — for example, if you want to change the format of your error response — you’d have to go into every single one of those routes and modify it. That’s when I learned about centralized error handling, and it completely changed how I approached error management.”


Part 1: Traditional Approach – Handling Errors in Every Route

Instructor:
“Let me show you what I mean by the traditional, non-centralized approach. In this method, every route manages its own errors. Let’s consider an example where we have a few routes — one for handling user requests and one for handling product requests. I had to manually check for errors in each one.”

js

CopyEdit

const express = require(‘express’);

const app = express();

// Route for handling user data

app.get(‘/user/:id’, (req, res) => {

  const user = null;  // Simulating that no user was found

  if (!user) {

    res.status(404).json({

      success: false,

      message: ‘User not found’

    });

  } else {

    res.json(user);

  }

});

// Route for handling product data

app.get(‘/product/:id’, (req, res) => {

  const product = null;  // No product found

  if (!product) {

    res.status(404).json({

      success: false,

      message: ‘Product not found’

    });

  } else {

    res.json(product);

  }

});

“In both routes, you can see that I’m handling the error by checking if the user or product exists, and then responding with a 404 error if not. This seems fine for a small application, but here’s the problem: as I added more routes to the app — for example, routes for handling order data, inventory management, or user authentication — I had to repeat this same error-checking logic over and over.

Now, what happens if I need to change the error response format? Let’s say I want to include a more detailed message or change the status code. In the old approach, I would have to go into every single route and update it manually. The code starts to get bloated, repetitive, and hard to maintain.”


Part 2: Introducing Centralized Error Handling

“That’s when I learned about centralized error handling, which allows me to handle all errors in a single place. Instead of repeating the same error checks in every route, I can delegate error handling to a central error handler. Let me show you how I refactored my code using this approach.”

  1. Create a Custom Error Class (ApiError)

“First, I created a custom error class called ApiError. This helps standardize errors across my app.”

js

CopyEdit

class ApiError extends Error {

  constructor(message, statusCode) {

    super(message);

    this.statusCode = statusCode;

    this.name = ‘ApiError’;

  }

}

module.exports = ApiError;

“This ApiError class is just an extension of the basic JavaScript Error class. It includes a statusCode property that we’ll use to define the HTTP status code for the error, making it easier to manage errors consistently throughout the app.”

  1. Use the Custom Error in Routes

“Next, instead of manually checking for errors inside each route, I throw the custom error wherever necessary and pass it to the central error handler.”

js

CopyEdit

const ApiError = require(‘./ApiError’); // Import ApiError

// Route for handling user data

app.get(‘/user/:id’, (req, res, next) => {

  const user = null;  // Simulating that no user was found

  if (!user) {

    return next(new ApiError(‘User not found’, 404));  // Pass the error to next()

  }

  res.json(user);

});

// Route for handling product data

app.get(‘/product/:id’, (req, res, next) => {

  const product = null;  // No product found

  if (!product) {

    return next(new ApiError(‘Product not found’, 404));  // Pass the error to next()

  }

  res.json(product);

});

“In these routes, instead of handling the error directly, I just throw an ApiError and pass it to the next() function. This tells Express to pass the error to the next middleware, which will be our central error handler.”

  1. Create the Centralized Error Handler

“Now, we need to create the actual error handler, which will catch any errors thrown in the app.”

js

CopyEdit

const errorHandler = (err, req, res, next) => {

  const statusCode = err.statusCode || 500;  // Default to 500 if no statusCode is provided

  res.status(statusCode).json({

    success: false,

    message: err.message || ‘Something went wrong’,

  });

};

app.use(errorHandler); // Register the error handler middleware

“The errorHandler middleware catches any errors passed through the next() function. It checks if the error has a statusCode. If not, it defaults to 500 (Internal Server Error). It then sends a JSON response with the error message. This way, no matter where the error happens, it gets handled in one place.”


Part 3: Advantages of Centralized Error Handling

“So, why is this approach better than handling errors in every route? Here are the key advantages of centralized error handling:”

  1. Single Point of Error Management:
    “With centralized error handling, all errors are handled in one place. You don’t need to repeat error-checking code in every route.”
  2. Consistency:
    “By using a custom error class, we ensure that all errors follow the same format, making it easier to debug and handle errors consistently.”
  3. Easier Maintenance:
    “Changes to error handling, such as changing the format of error messages or adding more details, can be made in just one place — the error handler. You don’t have to touch each route individually.”
  4. Cleaner Code:
    “Routes are now focused on business logic instead of error handling. This keeps the codebase clean, maintainable, and easy to understand.”

Conclusion

“Centralized error handling is a simple but powerful concept that makes your code more organized, easier to manage, and scalable. As your application grows, you’ll realize that having a centralized approach to error handling can save you a lot of time and effort. It’s something every developer should consider implementing from the start.

I encourage you all to refactor any code that handles errors in multiple places and move it to a centralized error handler. Trust me, it will make your code more maintainable in the long run.

Step 6: Refactoring to Centralize Error Handling

We will move the error handling logic to a centralized middleware, so that each controller only deals with business logic (e.g., fetching users/products), and the error-handling is delegated to the middleware.

File: src/models/ApiError.js

Let’s create a custom error class that we can throw in controllers.

js

CopyEdit

class ApiError extends Error {

  constructor(message, statusCode) {

    super(message);

    this.statusCode = statusCode;

    this.name = ‘ApiError’;

  }

}

module.exports = ApiError;

File: src/controllers/userController.js

Now, instead of directly sending the error response, we’ll throw an ApiError.

js

CopyEdit

const ApiError = require(‘../models/ApiError’);

exports.getUser = (req, res, next) => {

  const user = null; // Simulating that no user was found

  if (!user) {

    return next(new ApiError(‘User not found’, 404)); // Pass the error to the centralized error handler

  }

  res.json(user);

};

File: src/controllers/productController.js

Similarly, in productController.js, we’ll throw an error.

js

CopyEdit

const ApiError = require(‘../models/ApiError’);

exports.getProduct = (req, res, next) => {

  const product = null; // Simulating that no product was found

  if (!product) {

    return next(new ApiError(‘Product not found’, 404)); // Pass the error to the centralized error handler

  }

  res.json(product);

};

File: src/app.js

In app.js, we will add a centralized error-handling middleware at the end of the middleware chain.

js

CopyEdit

const express = require(‘express’);

const userRoutes = require(‘./routes/userRoutes’);

const productRoutes = require(‘./routes/productRoutes’);

const ApiError = require(‘./models/ApiError’);

const app = express();

// Use the routes

app.use(userRoutes);

app.use(productRoutes);

// Centralized error handler middleware

app.use((err, req, res, next) => {

  const statusCode = err.statusCode || 500; // Default to 500 for server errors

  res.status(statusCode).json({

    success: false,

    message: err.message || ‘Something went wrong’,

  });

});

// Start the server

app.listen(3000, () => {

  console.log(‘Server is running on http://localhost:3000’);

});

Step 7: Explanation

Now, error handling is centralized:

  • The controllers no longer handle errors directly. Instead, they pass errors to the next function using next(new ApiError(…)).
  • The centralized error handler at the end of the middleware chain catches any error passed to next() and sends a uniform error response to the client.

Step 8: Testing the Refactored Application

Run the app:

bash

CopyEdit

node src/app.js

Visiting:

  • http://localhost:3000/user/1 will now give you a response:

json

CopyEdit

{

  “success”: false,

  “message”: “User not found”

}

  • http://localhost:3000/product/1 will give you:

json

CopyEdit

{

  “success”: false,

  “message”: “Product not found”

}

Conclusion

By refactoring the code, we’ve centralized the error-handling logic into one middleware. This makes it easier to maintain and modify error-handling behavior, as any changes only need to be made in the app.js file, instead of modifying every controller.

This structure is cleaner and more scalable, especially in larger applications. The controllers now focus only on their core responsibility — handling business logic — and delegating error handling to a dedicated middleware.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *