Node.js Backend Fundamentals with Express · Build an API with Express
Central Error Handling
Handle every error in one place: Express 5 async errors, custom HTTP errors, safe messages in production and logging.
What you'll build
A central error handler that turns a custom HttpError into the right status, hides internal details of unexpected errors from clients, and logs them with the request ID.
The idea
- An error-handling middleware has four arguments:
(err, req, res, next), and is registered after all routes. - In Express 5, errors thrown in async handlers and rejected promises go there automatically.
- Use an error class for expected failures (not found, conflict, forbidden) with a status and safe message.
- For unexpected errors, log the details and send a generic 500 message: stack traces and SQL in responses leak internals.
The code
import express from "express";
class HttpError extends Error {
constructor(status, message) { super(message); this.status = status; }
}
const accounts = new Map([["ana", { balance: 30 }]]);
const app = express();
app.use(express.json());
app.post("/accounts/:name/withdraw", async (req, res) => {
const account = accounts.get(req.params.name);
if (!account) throw new HttpError(404, "account not found");
if (req.body.amount > account.balance) throw new HttpError(409, "insufficient funds");
account.balance -= req.body.amount;
res.json({ balance: account.balance });
});
app.get("/reports", async () => {
await Promise.resolve();
throw new Error("connection to reports-db.internal:5432 refused"); // an unexpected failure
});
const logged = [];
app.use((err, req, res, next) => {
const status = err instanceof HttpError ? err.status : 500;
if (status === 500) logged.push(`ERROR ${req.method} ${req.path}: ${err.message}`);
res.status(status).json({ error: status === 500 ? "internal server error" : err.message });
});
const server = app.listen(0);
const base = `http://127.0.0.1:${server.address().port}`;
const call = async (method, path, body) => {
const r = await fetch(base + path, { method, headers: { "Content-Type": "application/json" }, body: body && JSON.stringify(body) });
console.log(method, path, "->", r.status, JSON.stringify(await r.json()));
};
await call("POST", "/accounts/ana/withdraw", { amount: 20 });
await call("POST", "/accounts/ana/withdraw", { amount: 20 });
await call("POST", "/accounts/ben/withdraw", { amount: 5 });
await call("GET", "/reports");
server.close();
console.log("server log:", logged);
Output
POST /accounts/ana/withdraw -> 200 {"balance":10}
POST /accounts/ana/withdraw -> 409 {"error":"insufficient funds"}
POST /accounts/ben/withdraw -> 404 {"error":"account not found"}
GET /reports -> 500 {"error":"internal server error"}
server log: [
'ERROR GET /reports: connection to reports-db.internal:5432 refused'
]Run this on your computer — see “Get Started” for setup.
Common mistakes
- Sending
err.messageor stacks for all errors, leaking hostnames, SQL and file paths. - Error middleware with three parameters: Express only treats four-argument functions as error handlers.
try/catchin every handler that swallows errors or answers inconsistently.
Key points
- One error handler, registered last, with four parameters.
- Typed errors for expected failures; generic 500s for the unexpected.
- Log details server-side with a request ID; never return internals.
Further reading: Error handling in Express · OWASP Error Handling Cheat Sheet
Practice questions
How does Express recognise error-handling middleware?
Answer
By its four parameters (err, req, res, next); it must be registered after the routes.
Why not return err.message for every error?
Answer
Unexpected errors can contain internal details (hostnames, SQL, paths); clients should get a generic message while details go to logs.
When would you use a custom HttpError class?
Answer
For expected failures such as not found or conflicts, so handlers can throw them and the central handler maps them to the right status and safe message.
Open in the app to mark lessons complete, take notes and track your progress.