CodingNic

Error Handling

Custom Error Classes

Error Handling 12 min read

Custom Error Classes

Objectives

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

  • Create a custom error class extending JavaScript’s built-in Error
  • Attach a meaningful status code to a custom error type
  • Build a small hierarchy of specific error types for different failure situations

💡 Why this matters: A generic Error can’t distinguish “this record doesn’t exist” from “this input was invalid” from “the database is down,” even though each deserves a completely different HTTP status code and message. Custom error classes give every failure a real, checkable type.

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

A Base Application Error

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

class AppError extends Error (Module 2’s extends/super) creates a custom error type, super(message) calls Error’s own constructor, setting up .message and .stack correctly, exactly like extending any other built-in class. this.statusCode is a new property, not part of the built-in Error at all, carrying the HTTP status code this error should produce, right alongside the error itself.

Specific Error Types

javascript
class NotFoundError extends AppError {
  constructor(message) {
    super(message, 404);
    this.name = 'NotFoundError';
  }
}

class ValidationError extends AppError {
  constructor(message) {
    super(message, 400);
    this.name = 'ValidationError';
  }
}
javascript
const books = [{ id: 1, title: 'Refactoring' }];

app.get('/books/:id', (req, res, next) => {
  const book = books.find(b => b.id === Number(req.params.id));
  if (!book) {
    return next(new NotFoundError('Book not found'));
  }
  res.json(book);
});

app.get('/validate', (req, res, next) => {
  if (!req.query.name) {
    return next(new ValidationError('name is required'));
  }
  res.json({ name: req.query.name });
});
bash
curl -w " [%{http_code}]" http://localhost:4405/books/99
text
{"error":"NotFoundError","message":"Book not found"} [404]
bash
curl -w " [%{http_code}]" http://localhost:4405/validate
text
{"error":"ValidationError","message":"name is required"} [400]

NotFoundError and ValidationError both extend AppError, each hardcoding the correct status code (404, 400) in its own constructor, a route only needs to say what went wrong (new NotFoundError('Book not found')), not remember the right status code every single time, the error class itself already knows.

Custom Errors Work With Async Too

javascript
app.get('/async-fail', async (req, res) => {
  throw new NotFoundError('Async resource not found');
});
bash
curl -w " [%{http_code}]" http://localhost:4405/async-fail
text
{"error":"NotFoundError","message":"Async resource not found"} [404]

This combines directly with Lesson 2, Express 5 automatically catches the thrown NotFoundError from this async handler and forwards it to the error handler, exactly the same as the synchronous next(new NotFoundError(...)) calls above, instanceof checks and .statusCode work identically regardless of whether the error arrived via next() or an automatically-caught rejection.

Why This Matters at Scale

Without custom error types, an error handler (Lesson 5) has no reliable way to tell a “record not found” apart from “the database connection failed” beyond parsing error message strings, fragile and error-prone. With them, err instanceof NotFoundError or err.statusCode gives a reliable, checkable answer, and every route in an application can create exactly the specific error that describes its situation, new NotFoundError(...), new ValidationError(...), or others as a real application grows (an UnauthorizedError, a ConflictError), all sharing the same AppError base and the same handling logic downstream.

Try It

  1. Create an AppError base class and two subclasses of your own (for example UnauthorizedError with 401, ConflictError with 409).
  2. Use each in a route via next(new YourError('...')), and confirm the correct status code and message come back.
  3. Throw one of your custom errors from an async route handler with no try/catch, and confirm Express 5 still produces the correct status code.
  4. Explain, in your own words, why hardcoding the status code inside each error class (rather than passing it separately every time a route creates one) reduces the chance of a route returning the wrong status code.

Recap

  • class AppError extends Error creates a custom error type, carrying its own statusCode alongside the standard message.
  • Specific subclasses (NotFoundError, ValidationError, and similar) each hardcode the correct status code in their constructor, so a route only needs to describe what went wrong.
  • Custom errors work identically whether passed to next(err) explicitly or thrown from an async handler and caught automatically by Express 5.

Next lesson: a global error handler, one place where every error in the app ends up.