Creating Custom Errors in JavaScript: Complete Tutorial
Learn how to create custom error classes in JavaScript. Build domain-specific errors with extra properties, organize them in hierarchies, use them for typed error handling, and integrate them with serialization and logging.
JavaScript's built-in Error class is generic. When your application throws a TypeError, there is no way to tell from the type alone whether it came from user input validation, a network response parsing issue, or a programming mistake. Custom error classes solve this by creating named, typed errors that carry domain-specific information and enable precise error handling with instanceof checks.
Why Custom Errors?
// Without custom errors: guessing from message strings (fragile)
try {
await processOrder(data);
} catch (err) {
if (err.message.includes("not found")) {
// Fragile: what if the message changes?
}
if (err.message.includes("validation")) {
// Still fragile, and easy to get wrong
}
}
// With custom errors: precise, refactorable, not fragile
try {
await processOrder(data);
} catch (err) {
if (err instanceof NotFoundError) {
// Typed, reliable, searchable in the codebase
}
if (err instanceof ValidationError) {
// Can access err.field, err.violations, etc.
}
}Creating a Basic Custom Error
class AppError extends Error {
constructor(message) {
super(message); // Pass message to Error
this.name = "AppError"; // Override the name property (defaults to "Error")
}
}
// Test it
const err = new AppError("Something went wrong");
console.log(err.message); // "Something went wrong"
console.log(err.name); // "AppError"
console.log(err instanceof AppError); // true
console.log(err instanceof Error); // true (inheritance chain)
console.log(err.stack); // Stack traceThe Correct extends Pattern
There are two common patterns for setting name. The dynamic approach is better:
// Pattern A: hardcoded name (must update when class is renamed)
class NetworkError extends Error {
constructor(message) {
super(message);
this.name = "NetworkError"; // Must update if class name changes
}
}
// Pattern B: dynamic name (automatically reflects class name)
class NetworkError extends Error {
constructor(message) {
super(message);
this.name = this.constructor.name; // "NetworkError" — always correct
}
}
// Subclasses automatically get the right name:
class TimeoutError extends NetworkError {
constructor(ms) {
super(`Request timed out after ${ms}ms`);
this.name = this.constructor.name; // "TimeoutError"
this.ms = ms;
}
}Adding Custom Properties
Custom properties on errors carry context that helps both the error handler and the developer debugging:
class ValidationError extends Error {
constructor(message, field, value) {
super(message);
this.name = this.constructor.name;
this.field = field; // Which field failed
this.value = value; // What value was invalid
this.statusCode = 400; // HTTP status for API usage
}
}
class NotFoundError extends Error {
constructor(resource, id) {
super(`${resource} '${id}' not found`);
this.name = this.constructor.name;
this.resource = resource;
this.id = id;
this.statusCode = 404;
}
}
class AuthError extends Error {
constructor(reason = "Unauthorized") {
super(reason);
this.name = this.constructor.name;
this.statusCode = 401;
}
}
// Usage
function validateAge(age) {
if (typeof age !== "number") {
throw new ValidationError("Age must be a number", "age", age);
}
if (age < 0 || age > 150) {
throw new ValidationError("Age must be between 0 and 150", "age", age);
}
}
try {
validateAge("twenty");
} catch (err) {
if (err instanceof ValidationError) {
console.log(err.message); // "Age must be a number"
console.log(err.field); // "age"
console.log(err.value); // "twenty"
}
}Building an Error Hierarchy
A well-organized hierarchy lets you catch at different levels of specificity:
// Base: all app errors
class AppError extends Error {
constructor(message, options = {}) {
super(message, options); // ES2022: options.cause supported
this.name = this.constructor.name;
this.timestamp = new Date().toISOString();
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
}
}
// Level 2: domain categories
class DatabaseError extends AppError {}
class NetworkError extends AppError {}
class ValidationError extends AppError {
constructor(message, violations = []) {
super(message);
this.violations = violations;
}
}
// Level 3: specific errors
class ConnectionError extends DatabaseError {
constructor(host) {
super(`Cannot connect to database at ${host}`);
this.host = host;
}
}
class QueryError extends DatabaseError {
constructor(message, query) {
super(message);
this.query = query;
}
}
class TimeoutError extends NetworkError {
constructor(ms, endpoint) {
super(`Request to ${endpoint} timed out after ${ms}ms`);
this.ms = ms;
this.endpoint = endpoint;
}
}
// Handling at different levels:
try {
await doWork();
} catch (err) {
if (err instanceof ValidationError) {
// Handle validation specifically
return showFormErrors(err.violations);
}
if (err instanceof DatabaseError) {
// Handle any DB error (ConnectionError, QueryError both match)
return showDatabaseError(err);
}
if (err instanceof AppError) {
// Handle any known app error
return showGenericError(err);
}
// Unknown error: rethrow
throw err;
}Error Factory Functions
For very common errors, factory functions reduce repetition:
const Errors = {
notFound: (resource, id) => new NotFoundError(resource, id),
unauthorized: (msg) => new AuthError(msg),
validation: (field, msg) => new ValidationError(msg, field),
network: (msg, code) => new NetworkError(msg, code),
};
// Usage
throw Errors.notFound("User", userId);
throw Errors.validation("email", "Invalid email format");Serializing Custom Errors
The JSON.stringify problem and its solution:
// JSON.stringify misses Error properties (they're non-enumerable)
const err = new ValidationError("Invalid input", "email", "bad@");
console.log(JSON.stringify(err)); // "{}" — useless
// Add toJSON method for serialization
class AppError extends Error {
constructor(message, options = {}) {
super(message, options);
this.name = this.constructor.name;
}
toJSON() {
return {
name: this.name,
message: this.message,
stack: this.stack,
// Subclass properties are enumerable via spread:
...Object.fromEntries(
Object.entries(this).filter(([k]) => k !== "stack")
),
};
}
}
// Now JSON.stringify works:
JSON.stringify(err); // {"name":"ValidationError","message":"...","field":"email",...}Error Codes vs Error Classes
An alternative to instanceof hierarchy is error codes:
// Code-based approach (common in Node.js)
class AppError extends Error {
constructor(message, code) {
super(message);
this.code = code;
}
}
const ErrorCodes = {
NOT_FOUND: "NOT_FOUND",
VALIDATION_FAILED: "VALIDATION_FAILED",
UNAUTHORIZED: "UNAUTHORIZED",
};
throw new AppError("User not found", ErrorCodes.NOT_FOUND);
// Handling with codes:
if (err.code === ErrorCodes.NOT_FOUND) { ... }| Approach | Pros | Cons |
|---|---|---|
| instanceof hierarchy | Structured, inheritance, IDE autocompletion | More setup, tight coupling to class names |
| Error codes | Serializable, language-agnostic | No auto-complete, no inheritance semantics |
| Combined | Both instance type AND code | Slightly more code |
Rune AI
Key Insights
- Set this.name = this.constructor.name: This ensures the error prints with its class name rather than the misleading "Error", and works correctly for all subclasses without hardcoding
- Add domain-specific properties: Extra properties like
field,statusCode, orresourcemake errors self-describing and directly usable by handlers without string parsing - Build a hierarchy for layered catching: Parent classes let you catch broadly (
instanceof AppError) or narrowly (instanceof TimeoutError) based on what context you are in - Add toJSON for serialization: Error properties are non-enumerable and invisible to
JSON.stringifyby default — atoJSON()method fixes this for logging and API error responses - Error.captureStackTrace cleans up traces: This V8-specific call removes constructor noise from stack traces, pointing directly to where the error was thrown
Frequently Asked Questions
Does custom error instanceof break across iframes or realms?
Should I always extend Error or can I extend my base AppError?
How do I check for multiple error types in one condition?
Does Error.captureStackTrace matter?
Can custom errors carry nested errors as causes?
Conclusion
Custom error classes transform error handling from string guessing to reliable typed dispatch. The pattern is: extend Error (or your base class) with this.name = this.constructor.name, add domain-specific properties, and organize into a hierarchy that allows catching at different specificity levels. For the next step, see extending the JavaScript Error class for advanced patterns, and try/catch advanced error handling for using custom errors in real error handling flows.
More in this topic
OffscreenCanvas API in JS for UI Performance
Master the OffscreenCanvas API to offload rendering from the main thread. Covers worker-based 2D and WebGL rendering, animation loops inside workers, bitmap transfer, double buffering, chart rendering pipelines, image processing, and performance measurement strategies.
Advanced Web Workers for High Performance JS
Master Web Workers for truly parallel JavaScript execution. Covers dedicated and shared workers, structured cloning, transferable objects, SharedArrayBuffer with Atomics, worker pools, task scheduling, Comlink RPC patterns, module workers, and performance profiling strategies.
JavaScript Macros and Abstract Code Generation
Master JavaScript code generation techniques for compile-time and runtime metaprogramming. Covers AST manipulation, Babel plugin authorship, tagged template literals as macros, code generation pipelines, source-to-source transformation, compile-time evaluation, and safe eval alternatives.