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.

  1. The gas runs out. The cook does not shout at customers. They tell the manager.
  2. The manager tells customers: "Dosa is not available right now."
  3. The manager writes the real reason in the logbook for the owner.
  4. 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:

  1. The kinds of errors and how they reach Express.
  2. How Express 5 catches async errors, and where it cannot.
  3. How to write and place error middleware.
  4. How to build custom error classes.
  5. How to send 404s and HTML or JSON errors.
  6. 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 async handler: 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-timer gets a normal 500.
  • /timer crashes the process. The terminal shows the error and Node.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/abc gets a clear 400 with the real message.
  • /crash gets 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 Error and 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/broken never 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") or process.env.NODE_ENV to 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 .env to NODE_ENV=development and run again to see the details.

Simple lesson: Developers see details. Visitors see a safe message.

Common mistakes

MistakeWrong ideaCorrect ideaWhat goes wrong
Error middleware with 3 inputsAny middleware can catch errorsIt needs exactly 4Errors skip it
Error middleware above routesOrder does not matterRegister it lastIt never runs
Throwing inside setTimeoutExpress catches every throwOnly inside its own handlersThe server crashes
Not returning a promise chainExpress sees all promisesOnly returned or awaited onesErrors vanish or crash later
Sending err.message for 500sMessages are harmlessThey can hold secretsSQL and paths leak
404 handler after the error handlerEither order works404 first, then errorsUnknown paths get 500
Answering after headers were sentYou can always answerCheck res.headersSentERR_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 / catch plus next(err), or the server crashes.
  • Error middleware has four inputs and goes last. Check res.headersSent first.
  • 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.