Errors with meaning
Instead of throwing "bare" Errors, you define a custom error
class that carries useful information, such as the HTTP status code.
class ApiError extends Error {
constructor(message, statusCode) {
super(message); // sets this.message
this.name = "ApiError";
this.statusCode = statusCode;
}
}
Since ApiError extends Error, it is still instanceof Error, so
any code that already handles generic errors understands it, and it also
exposes statusCode so the error middleware can pick the response.
if (!user) {
throw new ApiError("User not found", 404);
}
Operational vs programmer errors
Distinguishing two families of errors helps decide how to respond:
- Operational (expected): invalid input, nonexistent resource, no
permissions. They are a normal part of operation. They carry a
statusCode4xx and a message meant for the client. They are handled and responded to clearly. - Programmer (bugs): an
undefinedthat shouldn't exist, a call to a nonexistent function. They are code failures. They are responded to generically with 5xx ("Internal error") to avoid leaking details, and they are logged so they can be fixed.
A good rule: only operational errors should carry a message and status meant for the user; programmer errors are hidden behind a 500.
Examples
An ApiError is still an Error
class ApiError extends Error {
constructor(message, statusCode) {
super(message);
this.name = "ApiError";
this.statusCode = statusCode;
}
}
const e = new ApiError("Unauthorized", 401);
console.log(e.message); // Unauthorized
console.log(e.statusCode); // 401
console.log(e instanceof Error); // true