CodingNic

Error Handling

The Global Error Handler

Error Handling 12 min read

The Global Error Handler

Objectives

By the end of this lesson, you should be able to:

  • Register a single global error handler as the last piece of middleware in an app
  • Distinguish an expected, “operational” error from an unexpected bug
  • Avoid leaking internal error details to a real client

💡 Why this matters: Every error from every route and middleware in this module, try/catch, next(err), an automatically-caught async rejection, custom error classes, ultimately needs exactly one place to actually decide what the client sees. That place is the global error handler.

⚠️ A note on verification: every snippet and every response shown below was actually run and tested with real HTTP requests.

Registering It Last

javascript
// every real route goes above this

// catch-all 404 for unmatched routes
app.use((req, res) => {
  res.status(404).json({ error: 'Not Found' });
});

// global error handler, always last
app.use((err, req, res, next) => {
  const statusCode = err.statusCode || 500;
  res.status(statusCode).json({ error: err.name, message: err.message });
});

Order matters here in two ways: the 404 handler (Module 7’s middleware-order rule) must come after every real route, or it would swallow requests that should have matched something, and the error handler, identified by its four parameters (Lesson 3), must be the very last piece of middleware registered, Express only recognizes it as an error handler by that four-parameter signature, position in the file doesn’t change that, but conventionally it’s kept last so every route and regular middleware appears above it.

Operational Errors vs Unexpected Bugs

Not every error is the same kind of problem. An operational error, a missing record, invalid input, is an expected, anticipated failure, using a custom error class (Lesson 4) with a known status code and a safe, useful message. An unexpected bug, a TypeError from a genuine mistake in the code, is not anticipated at all, and its real message and stack trace are exactly the kind of internal detail (Lesson 1) that shouldn’t reach a client:

javascript
class AppError extends Error {
  constructor(message, statusCode) {
    super(message);
    this.name = 'AppError';
    this.statusCode = statusCode;
    this.isOperational = true;
  }
}

app.get('/crash', (req, res) => {
  const data = null;
  console.log(data.property); // a genuine bug, not an AppError
});

app.use((err, req, res, next) => {
  const statusCode = err.statusCode || 500;
  if (err.isOperational) {
    console.log(`[operational] ${err.name}: ${err.message}`);
    res.status(statusCode).json({ error: err.name, message: err.message });
  } else {
    console.error('[unexpected]', err.stack);
    res.status(500).json({ error: 'InternalServerError', message: 'Something went wrong' });
  }
});
bash
curl -w " [%{http_code}]" http://localhost:4411/books/99
text
{"error":"NotFoundError","message":"Book not found"} [404]
bash
curl -w " [%{http_code}]" http://localhost:4411/crash
text
{"error":"InternalServerError","message":"Something went wrong"} [500]

err.isOperational, set by the AppError base class, distinguishes the two cases: an operational error’s specific name and message are genuinely useful to a client (NotFoundError, "Book not found"), an unexpected bug’s real message ("Cannot read properties of null") and stack trace are logged server-side only, with console.error, the client gets a generic, safe message instead. This is the single most important habit this module builds: real detail goes to the server’s logs, only safe, intentional detail goes to the client.

The Complete Picture

Every mechanism from this module funnels into this one handler: try/catch with next(err) (Lesson 1), an automatically-caught async rejection (Lesson 2), a manually called next(err) (Lesson 3), all carrying a custom error class (Lesson 4), all arriving at the exact same place, checked once, handled once, consistently, regardless of which route or middleware the error actually came from.

Try It

  1. Build an app with at least two routes using custom error classes (Lesson 4), a 404 catch-all, and a single global error handler distinguishing operational errors from unexpected bugs.
  2. Trigger an unexpected bug (not an AppError) and confirm the client receives a generic, safe message while the real error logs server-side.
  3. Trigger a known operational error and confirm the client receives its specific name, message, and status code.
  4. Explain, in your own words, why a single global error handler is preferable to handling errors differently, ad hoc, inside every individual route.

Recap

  • A global error handler, the last middleware registered, catches every error in the app, try/catch, next(err), and automatically-caught async rejections all arrive here.
  • Distinguishing operational errors (expected, safe to describe to a client) from unexpected bugs (log server-side, return a generic message) is the core discipline this module builds toward.
  • Never send a raw error message or stack trace from an unexpected bug to a real client, only an intentional, safe message.

Next lesson: this module’s exercises, building a complete error-handling setup from scratch.