Error handling is a crucial aspect of building robust applications. In Express.js, error handling can range from simple middleware to advanced techniques involving custom error classes, detailed stack traces, and centralized error management.This chapter covers everything you need to know about advanced error handling in Express.js, from the basics of handling errors to creating reusable and powerful error management systems.
Error handling in Express.js is typically managed using middleware. Errors can be captured, logged, and sent as responses to clients. By understanding and extending this basic mechanism, you can handle errors more effectively.
Express has a default error-handling mechanism that responds with an HTML stack trace for development or a generic error for production. However, customizing this behavior is critical for production-grade apps.
const express = require("express");
const app = express();
// Simulate an error
app.get("/", (req, res) => {
throw new Error("Something went wrong!");
});
// Default error handling middleware
app.use((err, req, res, next) => {
console.error(err.stack); // Logs the error stack
res.status(500).send("Internal Server Error");
});
app.listen(3000, () => {
console.log("Server running on http://localhost:3000");
});
throw new Error()
: Triggers an error.err.stack
: Provides detailed stack trace information.Custom error classes help structure error data, making it easier to handle specific error types and provide meaningful responses.
class AppError extends Error {
constructor(message, statusCode) {
super(message); // Call parent constructor
this.statusCode = statusCode;
this.isOperational = true;
// Capture the stack trace
Error.captureStackTrace(this, this.constructor);
}
}
module.exports = AppError;
AppError
: A custom class that extends the built-in Error
class.statusCode
: HTTP status code for the error.isOperational
: A flag to differentiate expected errors from programming bugs.Error.captureStackTrace
: Ensures the stack trace points to the relevant location.
const express = require("express");
const AppError = require("./AppError");
const app = express();
app.get("/", (req, res, next) => {
return next(new AppError("Route not found", 404));
});
// Centralized Error Handler
app.use((err, req, res, next) => {
const statusCode = err.statusCode || 500;
const message = err.message || "Internal Server Error";
res.status(statusCode).json({
status: "error",
statusCode,
message,
});
});
app.listen(3000, () => {
console.log("Server running on http://localhost:3000");
});
{
"status": "error",
"statusCode": 404,
"message": "Route not found"
}
Error-handling middleware is identified by having four arguments: (err, req, res, next)
.
const errorLogger = (err, req, res, next) => {
console.error(`[${new Date().toISOString()}] ${err.message}`);
next(err);
};
const errorResponder = (err, req, res, next) => {
res.status(err.statusCode || 500).json({
error: err.message || "Internal Server Error",
});
};
const failSafeHandler = (err, req, res, next) => {
res.status(500).send("Something went terribly wrong!");
};
app.use(errorLogger);
app.use(errorResponder);
app.use(failSafeHandler);
Async functions can throw errors that may not be caught directly by Express. A wrapper can ensure these errors are passed to the error-handling middleware.
const asyncHandler = (fn) => (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
app.get(
"/async",
asyncHandler(async (req, res) => {
const data = await fetchData(); // Simulate async operation
res.json(data);
})
);
asyncHandler
: Wraps asynchronous functions to catch and forward errors.Stack traces help debug issues by showing where the error originated.
app.use((err, req, res, next) => {
console.error("Stack Trace:", err.stack);
res.status(500).send("An error occurred!");
});
Use a logging library like winston
or pino
for structured logging.
const winston = require("winston");
const logger = winston.createLogger({
level: "error",
format: winston.format.json(),
transports: [new winston.transports.File({ filename: "error.log" })],
});
app.use((err, req, res, next) => {
logger.error(`${err.message} - ${req.originalUrl} - ${req.method} - ${req.ip}`);
res.status(500).send("Logged and handled!");
});
Handle application crashes gracefully to close open connections and release resources.
process.on("uncaughtException", (err) => {
console.error("Uncaught Exception:", err);
process.exit(1);
});
process.on("unhandledRejection", (reason) => {
console.error("Unhandled Rejection:", reason);
process.exit(1);
});
Advanced error handling in Express.js transforms a basic application into a production-ready one. By using custom error classes, stack traces, and centralized middleware, you can make error handling structured and efficient. Integrating logging tools ensures proper monitoring, while async wrappers and graceful shutdowns prepare your application for real-world challenges. These techniques not only enhance user experience but also simplify debugging and maintenance. Happy coding !❤️