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.

JavaScriptintermediate
12 min read

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?

javascriptjavascript
// 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

javascriptjavascript
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 trace

The Correct extends Pattern

There are two common patterns for setting name. The dynamic approach is better:

javascriptjavascript
// 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:

javascriptjavascript
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:

javascriptjavascript
// 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:

javascriptjavascript
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:

javascriptjavascript
// 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:

javascriptjavascript
// 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) { ... }
ApproachProsCons
instanceof hierarchyStructured, inheritance, IDE autocompletionMore setup, tight coupling to class names
Error codesSerializable, language-agnosticNo auto-complete, no inheritance semantics
CombinedBoth instance type AND codeSlightly more code
Rune AI

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, or resource make 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.stringify by default — a toJSON() 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
RunePowered by Rune AI

Frequently Asked Questions

Does custom error instanceof break across iframes or realms?

Yes. `instanceof` checks the prototype chain against the class from the current JavaScript realm. An `AppError` from one iframe is not `instanceof AppError` from another iframe's class. Use duck-typing (`err.name === "AppError"`) for cross-realm checks.

Should I always extend Error or can I extend my base AppError?

Extend your base `AppError` for domain errors — it centralizes `name` setting, stack capture, and any shared properties like `timestamp`. Reserve direct `Error` extension for your base AppError class.

How do I check for multiple error types in one condition?

JavaScript has no `catch (err instanceof X or Y)` syntax. Use the logical OR pattern: ```javascript if (err instanceof NotFoundError || err instanceof ValidationError) { ... } ``` Or if they share a base, catch the base class.

Does Error.captureStackTrace matter?

`Error.captureStackTrace(this, this.constructor)` is a V8-specific optimization that removes the constructor frames from the stack trace, making the trace point to the throw site rather than inside the error class constructor. It is optional but improves debug readability in Node.js and Chrome.

Can custom errors carry nested errors as causes?

Yes, use the `cause` option: `new AppError("Outer failure", { cause: innerError })`. The cause is accessible as `err.cause`. Supported in Node.js 16.9+ and modern browsers. For older environments, add `this.cause = options.cause` manually in the constructor.

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.