class AppError {
constructor(message, code) {
this.message = message;
this.code = code;
this.name = 'AppError';
// NOT extending Error β no stack trace, no instanceof Error!
}
}
try {
throw new AppError('Something broke', 500);
} catch (e) {
console.log(e instanceof Error); // false!
console.log(e.stack); // undefined!
console.log(e.name); // 'AppError'
}class AppError extends Error {
constructor(message, code) {
super(message); // β
sets .message and .stack
this.name = 'AppError'; // β
override default 'Error'
this.code = code;
// V8-specific: captures stack from HERE not from Error constructor
if (Error.captureStackTrace) {
Error.captureStackTrace(this, AppError);
}
}
}
// Specific subtypes
class NotFoundError extends AppError {
constructor(resource) {
super(`${resource} not found`, 404);
this.name = 'NotFoundError';
}
}
try {
throw new NotFoundError('User');
} catch (e) {
console.log(e instanceof Error); // true β
console.log(e instanceof AppError); // true β
console.log(e.name); // 'NotFoundError'
console.log(e.message); // 'User not found'
console.log(typeof e.stack); // 'string' β
}Bug: AppError doesn't extend Error, so it has no stack property, fails instanceof Error checks, and won't be caught by catch clauses that check instanceof Error.
Explanation: Always extend Error for custom errors. super(message) sets .message and .stack. Set this.name manually because it defaults to "Error". Error.captureStackTrace makes the stack start at the throw site.
Key Insight: Custom errors MUST extend Error to: get a stack trace, work with instanceof Error, integrate with error monitoring tools, and behave as real errors in try/catch.