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
Errorcan’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
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
class NotFoundError extends AppError {
constructor(message) {
super(message, 404);
this.name = 'NotFoundError';
}
}
class ValidationError extends AppError {
constructor(message) {
super(message, 400);
this.name = 'ValidationError';
}
}
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 });
});
curl -w " [%{http_code}]" http://localhost:4405/books/99
{"error":"NotFoundError","message":"Book not found"} [404]
curl -w " [%{http_code}]" http://localhost:4405/validate
{"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
app.get('/async-fail', async (req, res) => {
throw new NotFoundError('Async resource not found');
});
curl -w " [%{http_code}]" http://localhost:4405/async-fail
{"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
- Create an
AppErrorbase class and two subclasses of your own (for exampleUnauthorizedErrorwith401,ConflictErrorwith409). - Use each in a route via
next(new YourError('...')), and confirm the correct status code and message come back. - Throw one of your custom errors from an
asyncroute handler with notry/catch, and confirm Express 5 still produces the correct status code. - 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 Errorcreates a custom error type, carrying its ownstatusCodealongside the standardmessage.- 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 anasynchandler and caught automatically by Express 5.
Next lesson: a global error handler, one place where every error in the app ends up.