Global Error Handling Middleware & Custom AppError Classes0%

Global Error Handling Middleware & Custom AppError Classes

Beginner12 min read•Updated: Jul 11, 2026
Study MaterialsCheatsheet

Global Error Handling Middleware & Custom AppError Classes

Uncaught exceptions and unhandled promise rejections can crash a Node.js production server, terminating all active client connections. Building a resilient backend requires a centralized global error-handling architecture with custom operational error classes.


1. Operational Errors vs Programmer Bugs

  • Operational Errors: Predictable, unavoidable runtime errors (e.g., invalid user input, invalid login password, database duplicate key, file not found). These must be caught and returned as structured HTTP client errors (4xx).
  • Programmer Bugs: Unintentional bugs in code (e.g., calling .map() on undefined, syntax errors, reading missing properties). These must be logged with full stack traces and returned as 500 Internal Server Error.

2. Building a Custom AppError Class

JavaScript
// src/utils/AppError.js
export class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.status = `${statusCode}`.startsWith('4') ? 'fail' : 'error';
this.isOperational = true; // Flag identifying predictable operational errors
 
Error.captureStackTrace(this, this.constructor);
}
}

3. The catchAsync Wrapper Utility

Instead of polluting every controller with repetitive try/catch boilerplate, use an asynchronous higher-order function wrapper:

JavaScript
// src/utils/catchAsync.js
export const catchAsync = (fn) => {
return (req, res, next) => {
fn(req, res, next).catch(next); // Catches rejected promises and forwards to next(err)
};
};
 
// Usage in controllers:
export const getProfile = catchAsync(async (req, res, next) => {
const user = await User.findById(req.params.id);
if (!user) {
return next(new AppError('User not found with that ID', 404));
}
res.status(200).json({ status: 'success', data: user });
});

4. Centralized Global Error Handling Middleware

Place this middleware at the very end of all route definitions:

JavaScript
// src/middleware/errorHandler.js
export function errorHandler(err, req, res, next) {
err.statusCode = err.statusCode || 500;
err.status = err.status || 'error';
 
if (process.env.NODE_ENV === 'development') {
// Development: Return rich debugging info and stack trace
res.status(err.statusCode).json({
status: err.status,
error: err,
message: err.message,
stack: err.stack
});
} else {
// Production: Leak zero implementation details for unhandled bugs
if (err.isOperational) {
res.status(err.statusCode).json({
status: err.status,
message: err.message
});
} else {
console.error('CRITICAL UNHANDLED ERROR 💥:', err);
res.status(500).json({
status: 'error',
message: 'Something went wrong on our end. Please try again later.'
});
}
}
}

Interactive Quiz

Multiple Choice Questions

Question 1
Why must the global error-handling middleware be declared with exactly four parameters (err, req, res, next)?
Question 2
What is the danger of returning raw err.stack traces to clients in production environments?
Question 3
What does the catchAsync wrapper pattern accomplish in Express controllers?
Question 4
What is an "operational error" in backend architecture?
Question 5
Where should the global error middleware app.use(errorHandler) be mounted in the Express application?

Next Lesson

Input Validation & Data Sanitization with Joi or Zod

Continue learning with hands-on practice, examples, and exercises in the upcoming topic.

Practice Quiz

Test your understanding of this lesson with 5 questions. Each question has one correct answer.

PrevNext