Express catches errors thrown anywhere in the request-response cycle and routes them to error-handling middleware, so one bad request doesn't take down the whole server.
Why it matters
- Keeps the application stable when something unexpected happens.
- Lets you log errors for debugging.
- Lets you return clean, non-technical messages to clients instead of stack traces.
1. Default error handler
If you don't define your own, Express falls back to a generic 500 Internal Server Error.
const express = require("express");
const app = express();
app.get("/", (req, res) => {
throw new Error("Inventory lookup failed");
});
app.listen(5100, () => console.log("Server running on port 5100"));
2. Custom error-handling middleware
app.get("/", (req, res) => {
throw new Error("Inventory lookup failed");
});
app.use((err, req, res, next) => {
console.log(err);
res.status(500).json({ message: "Oops, something went wrong on our end." });
});
3. Synchronous errors
Express automatically forwards errors thrown inside synchronous route handlers.
app.get("/sync-error", (req, res) => {
throw new Error("Synchronous failure!");
});
app.use((err, req, res, next) => {
res.status(500).json({ message: err.message });
});
4. Asynchronous errors
Errors inside async functions must be caught manually and passed to next().
app.get("/async-error", async (req, res, next) => {
try {
await Promise.reject(new Error("Async failure!"));
} catch (err) {
next(err);
}
});
app.use((err, req, res, next) => {
res.status(500).json({ message: err.message });
});
5. Manually forwarding errors with next(err)
app.get("/manual-error", (req, res, next) => {
const err = new Error("Triggered manually!");
next(err);
});
app.use((err, req, res, next) => {
res.status(500).json({ message: err.message });
});
Whichever pattern you use, always define error-handling middleware after your routes, with
all four parameters (err, req, res, next) — that four-parameter signature is what
tells Express it's an error handler.