Custom Error Hierarchies
Elevate your error handling from generic strings to semantic object hierarchies. Learn to use `Error.captureStackTrace` and class inheritance to build self-documenting, resilient application logic.
Why Extend the Error Class?
In a production system, a simple `throw new Error("fail")` is rarely sufficient. Engineering teams need to distinguish between **Operational Errors** (like a 404 from an API) and **Programmer Bugs** (like a null pointer). By extending the base `Error` class, you can attach critical metadata such as HTTP status codes, internal error identifiers, and recovery hints.
// Professional Pattern: The Base Application Error
class AppError extends Error {
constructor(message, statusCode = 500) {
super(message);
this.name = this.constructor.name;
this.statusCode = statusCode;
this.isOperational = true; // For distinguishing programming bugs from operational errors
// Technical Insight: Capturing the Stack Trace
// This keeps the constructor call out of the stack trace
Error.captureStackTrace(this, this.constructor);
}
}
class ValidationError extends AppError {
constructor(message, fields) {
super(message, 400);
this.fields = fields; // Attach field-specific validation failures
}
}The Power of Instanceof
The primary benefit of a class-based error hierarchy is the ability to use the `instanceof` operator in your `catch` blocks. This allows you to implement specific recovery logic (e.g., retrying a network request or redirecting a user to a login page) without parsing fragile error message strings.
// Domain-Driven Design: Error Hierarchies
class PaymentsError extends AppError {}
class InsufficientFundsError extends PaymentsError {
constructor(amountNeeded) {
super(`Transaction failed: $${amountNeeded} required`, 402);
}
}
// Usage in an orchestrator
try {
processPayment(user, 100);
} catch (error) {
if (error instanceof InsufficientFundsError) {
promptUserForTopUp(error.message);
} else if (error instanceof PaymentsError) {
logToSecurityTeam(error);
}
}Technical Insight: captureStackTrace
V8 (the engine behind Chrome and Node.js) provides `Error.captureStackTrace(target, constructor)`. In a custom error class, calling this ensures that the stack trace points directly to where the error was *thrown*, rather than the line inside your custom error's `constructor`. This drastically improves "time to resolution" during debugging.
Custom Error Best Practices:
- ✅ **Inheritance:** Always extend from a base `AppError` to share common logic across the system.
- ✅ **Metadata:** Attach machine-readable codes (e.g., `E_AUTH_EXPIRED`) alongside human messages.
- ✅ **Clean Stacks:** Use `captureStackTrace` to keep your debugging logs focused.
- ✅ **Semantic Naming:** Name your classes after the *problem*, not the *solution* (e.g., `TimeoutError`).
- ✅ **Domain Boundaries:** Group errors by module (e.g., `StorageError`, `NetworkError`).