Error Handling in Express.js
By Syncfusion®, Inc. · Last updated
Operational and programming errors, how errors reach Express, Express 5 async forwarding and its limits, error middleware, res.headersSent, custom error classes, 404 handling, HTML and JSON errors, and development versus production error output.
What you will learn
- Explain how errors reach Express
- Catch errors in callbacks and timers
- Write central error middleware
- Create custom error classes
- Hide error details in production
Error Handling Introduction
Things go wrong in every server. Good error handling decides what happens next.
- Error = something stopped your code from doing its job.
- Express has a built-in default error handler. It sends a plain HTML error page.
- Error middleware = your own handler with four inputs:
(err, req, res, next). - Good handling sends a clear, safe answer and logs the details for you.
- Bad handling crashes the server or leaks secrets.
Simple lesson: Catch errors in one place, answer safely, and log the details.
A Real-World Example
The canteen has a problem desk.
- The gas runs out. The cook does not shout at customers. They tell the manager.
- The manager tells customers: "Dosa is not available right now."
- The manager writes the real reason in the logbook for the owner.
- If no one tells the manager, customers just wait at the counter forever.
The cook calling the manager is next(err). The manager is your error middleware. The logbook is your server log.
In this lesson, you will learn:
- The kinds of errors and how they reach Express.
- How Express 5 catches async errors, and where it cannot.
- How to write and place error middleware.
- How to build custom error classes.
- How to send 404s and HTML or JSON errors.
- How to keep error details out of production answers.
Kinds of Errors
- Operational errors: expected problems, such as "item not found" or "database is down".
- Programming errors: bugs in your code, such as reading a property of
undefined. - Synchronous errors: thrown straight away in normal code.
- Asynchronous errors: happen later, after
await, in a callback or in a timer. - Operational errors get a helpful answer. Programming errors get a generic 500, and a fix.
Programming errors can leave the server in a strange state. Many teams log them, alert someone and restart the process. Operational errors are normal and just need a good answer.
Simple lesson: Expected problems get clear answers. Bugs get a safe 500 and a fix.
How Errors Reach Express
- throw inside a handler: Express catches it.
- next(err): you hand the error to Express yourself.
- A rejected promise from an
asynchandler: Express 5 catches it for you. - In all three cases, Express skips normal middleware and runs error middleware.
- With no error middleware, the default error handler answers.
- All three answer 500 with the default HTML page.
- The terminal shows each error message.
Simple lesson: throw, next(err) and async rejections all reach error handling.
Where Express 5 Cannot Help
Express 5 only sees errors that happen inside its handlers or their returned promises.
- Callbacks and timers run later, outside Express. A throw there crashes the whole server.
- Promises you do not return or await are invisible to Express.
- Errors outside handlers, such as during startup, are not Express errors at all.
- In those places, catch the error yourself and call next(err).
- Express 4 needed
next(err)for every async error. Express 5 does it for returned promises.
/safe-timergets a normal 500./timercrashes the process. The terminal shows the error andNode.js v22.- After that, every request fails, because the server is gone. Press ↻ Restart.
One uncaught error in a callback stops the server for every user. Wrap callback and timer code in try / catch and call next(err).
Simple lesson: Inside callbacks and timers, catch errors yourself and call next(err).
Error Middleware
- Error middleware has exactly four inputs:
(err, req, res, next). - Express knows it by the number of inputs. Three inputs means normal middleware.
- Register it after all routes and routers.
- One central error middleware keeps every error answer the same.
- Call
next(err)inside it to pass the error to the next error handler.
/menu/abcgets a clear 400 with the real message./crashgets a generic 500. The real bug is only in the terminal.
When Headers Are Already Sent
- Sometimes an error happens after the response started, such as midway through a stream.
- You cannot send a new status then. res.headersSent is
true. - In that case, call
next(err). The default handler closes the connection.
app.use((err, req, res, next) => {
if (res.headersSent) return next(err); // too late to answer
res.status(err.status ?? 500).json({ error: "Something went wrong" });
});
Simple lesson: Four inputs, registered last, and hand off if headers were already sent.
Custom Error Classes
Give each kind of operational error its own class.
- Extend
Errorand add a status and an application error code. - Status = the HTTP code, such as 404.
- Error code = a fixed label for programs, such as
NOT_FOUND. - Safe message = text you are happy for users to read.
- Internal details = extra data for your logs only.
- Known errors keep their status, code and message.
- The database message in
/api/brokennever reaches the client.
Simple lesson: Throw named errors with a status and a code. One handler turns them into answers.
Not-Found Handling
- A request that matches no route is not an error. Express just runs out of routes.
- Add 404 middleware after all routes: a normal
(req, res, next)function. - Put it before the error middleware.
- APIs answer JSON. Websites answer an HTML page.
- You can also turn it into an error with
next(new NotFoundError(...)), so the error handler formats it.
Easy way to remember the order: routes, then 404, then errors.
Simple lesson: A 404 handler is normal middleware, placed after routes and before error middleware.
Development vs Production Errors
- Development: show the error and stack, so you can fix it quickly.
- Production: show a short, safe message. Log the details.
- Use
app.get("env")orprocess.env.NODE_ENVto choose. - Never send stacks, SQL, file paths, tokens or passwords in production.
- Include a request ID in both the answer and the log, so you can match them. See Logging and Debugging.
- In production, the client never sees the file path.
- Change
.envtoNODE_ENV=developmentand run again to see the details.
Simple lesson: Developers see details. Visitors see a safe message.
Common mistakes
| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| Error middleware with 3 inputs | Any middleware can catch errors | It needs exactly 4 | Errors skip it |
| Error middleware above routes | Order does not matter | Register it last | It never runs |
Throwing inside setTimeout | Express catches every throw | Only inside its own handlers | The server crashes |
| Not returning a promise chain | Express sees all promises | Only returned or awaited ones | Errors vanish or crash later |
Sending err.message for 500s | Messages are harmless | They can hold secrets | SQL and paths leak |
| 404 handler after the error handler | Either order works | 404 first, then errors | Unknown paths get 500 |
| Answering after headers were sent | You can always answer | Check res.headersSent | ERR_HTTP_HEADERS_SENT |
Check Yourself
Exercises
What to Remember
- throw,
next(err)and rejected async promises all reach error handling. - Callbacks and timers need
try/catchplusnext(err), or the server crashes. - Error middleware has four inputs and goes last. Check
res.headersSentfirst. - Custom error classes carry a status, a code and a safe message.
- Routes, then 404, then errors. HTML for pages, JSON for APIs.
- In production, log details and send only safe messages.
Next: Token Authentication and Authorization. You will log users in with JWTs and protect routes with roles.