Express Error Handling — Complete Guide to Error Management in Express
In this tutorial, you will learn about Express Error Handling. We cover key concepts, practical examples, and best practices to help you master this topic.
Express error handling uses centralized error middleware to catch and respond to errors consistently, preventing crashes and providing clear feedback to clients.
What You'll Learn
By the end of this tutorial, you'll implement centralized Express error handling, create custom error classes, wrap async route handlers, handle 404s, and differentiate dev/production error responses.
Why Error Handling Matters
Without centralized error handling, every route must duplicate error logic. An unhandled async error crashes the server. Consistent error handling improves debugging and user experience.
Real-World Use
An Express API with 50 routes has one error middleware. When any route throws an error, the middleware logs the details, sanitizes the response, and returns a consistent JSON error format.
Express Error Handling Learning Path
flowchart LR
A[Middleware] --> B[Error Handling]
B --> C[Template Engines]
C --> D[Sessions]
D --> E[Security]
A --> F{You Are Here}
style F fill:#f90,color:#fff
The Problem: Async Route Errors
// This crashes the server on error
app.get("/users/:id", async (req, res) => {
const user = await db.findUser(req.params.id); // If this throws...
res.json(user);
});
Express does not catch promise rejections. An unhandled rejection in async route handlers crashes the server.
Wrapper for Async Handlers
const asyncHandler = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
app.get("/users/:id", asyncHandler(async (req, res) => {
const user = await db.findUser(req.params.id);
if (!user) throw new NotFoundError("User");
res.json(user);
}));
Custom Error Classes
class AppError extends Error {
constructor(message, statusCode = 500) {
super(message);
this.statusCode = statusCode;
this.isOperational = true;
Error.captureStackTrace(this, this.constructor);
}
}
class NotFoundError extends AppError {
constructor(resource = "Resource") {
super(`${resource} not found`, 404);
}
}
class ValidationError extends AppError {
constructor(message) {
super(message, 400);
}
}
Centralized Error Middleware
// Must be registered AFTER all routes
app.use((err, req, res, next) => {
err.statusCode = err.statusCode || 500;
err.status = err.status || "error";
if (process.env.NODE_ENV === "development") {
res.status(err.statusCode).json({
status: err.status,
error: err,
message: err.message,
stack: err.stack
});
} else {
res.status(err.statusCode).json({
status: err.status,
message: err.isOperational ? err.message : "Something went wrong"
});
}
});
404 Handler
// Must be after all routes but before error middleware
app.all("*", (req, res) => {
res.status(404).json({
status: "fail",
message: `Route ${req.originalUrl} not found`
});
});
Global Uncaught Handlers
process.on("uncaughtException", (err) => {
console.error("UNCAUGHT EXCEPTION. Shutting down...");
console.error(err.name, err.message);
process.exit(1);
});
process.on("unhandledRejection", (err) => {
console.error("UNHANDLED REJECTION. Shutting down...");
console.error(err.name, err.message);
server.close(() => process.exit(1));
});
Common Mistakes
1. Not Catching Async Errors
Async route errors without catch crash the server. Use asyncHandler wrapper for all async routes.
2. Error Middleware Before Routes
Error middleware must be registered after all routes. Otherwise, it won't catch route errors.
3. Leaking Error Details in Production
Stack traces and internal messages expose application internals. Return generic messages to clients in production.
4. Duplicate Error Handling
Don't add try/catch in every route when you have centralized error middleware. Throw errors and let middleware handle them.
5. Not Differentiating Error Types
Use custom error classes to distinguish validation errors (400), auth errors (401), not found (404), and server errors (500).
Practice Questions
1. Why doesn't Express catch async errors in route handlers?
Express only catches synchronous throws. Async errors return rejected promises that need explicit handling with .catch(next) or async wrapper.
2. What is the purpose of the asyncHandler wrapper?
It catches promise rejections and passes them to next(), which forwards them to Express error middleware.
3. How is error middleware different from regular middleware?
It has four parameters (err, req, res, next). Express identifies it by the function signature.
4. What should the 404 handler do?
Catch requests that don't match any route and return a 404 response with clear messaging about the invalid path.
5. Challenge: Create an Express app with centralized error handling for a simple API.
import express from "express";
const app = express();
const asyncHandler = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
class NotFoundError extends Error { constructor(m) { super(m); this.statusCode = 404; } }
app.get("/users/:id", asyncHandler(async (req, res) => {
throw new NotFoundError("User not found");
}));
app.all("*", (req, res) => res.status(404).json({ error: "Not found" }));
app.use((err, req, res, next) => {
res.status(err.statusCode || 500).json({ error: err.message || "Server error" });
});
app.listen(3000);
FAQ
Mini Project: Error Handler with Slack Notifications
Build error middleware that sends critical errors to Slack.
app.use(async (err, req, res, next) => {
if (err.statusCode >= 500) {
try {
await fetch(process.env.SLACK_WEBHOOK_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
text: `Server Error: ${err.message}\nPath: ${req.method} ${req.url}`
})
});
} catch (slackErr) { console.error("Slack notification failed"); }
}
res.status(err.statusCode || 500).json({ error: err.message });
});
What's Next
Express Template Engines Express Sessions Express Security
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro