JavaScript Custom Errors

Custom errors are your own error classes that extend JavaScript's built-in Error class. Instead of throwing a plain "Error: something went wrong", you create meaningful, named error types — like ValidationError, NotFoundError, or NetworkError — that make your code easier to debug and test.

Why Create Custom Errors?

When you catch an error in a large application, a plain Error tells you that something broke but not what kind of problem it was. A custom error like DatabaseConnectionError or AuthenticationError tells you exactly what went wrong at a glance — and lets you handle each type differently in catch blocks.

Diagram: Plain Error vs Custom Error

Plain Error:
  error.name    = "Error"
  error.message = "something failed"
  → Unclear — what failed? Why?

Custom Error:
  error.name    = "ValidationError"
  error.message = "Email format is invalid"
  error.field   = "email"
  → Crystal clear — exactly what and where

Creating a Custom Error Class

Extend the built-in Error class using class and extends. Call super(message) inside the constructor to set the error message.

class ValidationError extends Error {
  constructor(message) {
    super(message);          // sets error.message
    this.name = "ValidationError"; // overrides error.name
  }
}

// Using it
try {
  throw new ValidationError("Email is required");
} catch (error) {
  console.log(error.name);    // "ValidationError"
  console.log(error.message); // "Email is required"
  console.log(error instanceof ValidationError); // true
  console.log(error instanceof Error);            // true (inherits!)
}

Diagram: Custom Error Inheritance

Error (built-in)
  │
  ├─ name
  ├─ message
  └─ stack

ValidationError extends Error
  │
  ├─ name    = "ValidationError"
  ├─ message = (whatever you pass)
  └─ stack   (inherited automatically)

Adding Extra Properties

Custom errors can carry additional context — like which field failed, an error code, or an HTTP status.

class ValidationError extends Error {
  constructor(message, field) {
    super(message);
    this.name  = "ValidationError";
    this.field = field;      // extra context
  }
}

try {
  throw new ValidationError("Must be a valid email", "email");
} catch (error) {
  console.log(error.name);    // "ValidationError"
  console.log(error.message); // "Must be a valid email"
  console.log(error.field);   // "email"
}

Multiple Custom Error Types

Create one error type per category of problem in your application.

class NotFoundError extends Error {
  constructor(resource) {
    super(resource + " not found");
    this.name = "NotFoundError";
    this.resource = resource;
  }
}

class NetworkError extends Error {
  constructor(statusCode, message) {
    super(message);
    this.name = "NetworkError";
    this.statusCode = statusCode;
  }
}

class AuthenticationError extends Error {
  constructor(message = "Authentication failed") {
    super(message);
    this.name = "AuthenticationError";
  }
}

Handling Different Error Types in catch

Use instanceof to detect the error type and respond appropriately.

function loadUserProfile(userId) {
  if (!userId) {
    throw new ValidationError("User ID is required", "userId");
  }
  if (userId === 999) {
    throw new NotFoundError("User");
  }
  if (userId === 0) {
    throw new AuthenticationError("You must be logged in");
  }
  return { id: userId, name: "Priya" };
}

function handleProfileLoad(userId) {
  try {
    let profile = loadUserProfile(userId);
    console.log("Profile loaded:", profile.name);
  } catch (error) {
    if (error instanceof ValidationError) {
      console.log("Fix your input:", error.message, "| Field:", error.field);
    } else if (error instanceof NotFoundError) {
      console.log("Not found:", error.message);
    } else if (error instanceof AuthenticationError) {
      console.log("Login required:", error.message);
    } else {
      console.log("Unexpected error:", error.message);
      throw error; // re-throw unknown errors
    }
  }
}

handleProfileLoad(null); // Fix your input: User ID is required | Field: userId
handleProfileLoad(999);  // Not found: User not found
handleProfileLoad(0);    // Login required: Authentication failed
handleProfileLoad(5);    // Profile loaded: Priya

Diagram: Error Routing in catch

catch (error)
   │
   ├─ instanceof ValidationError    → show field validation message
   │
   ├─ instanceof NotFoundError      → show "not found" UI
   │
   ├─ instanceof AuthenticationError → redirect to login
   │
   └─ else (unknown error)          → log and re-throw

Building an Error Hierarchy

You can create a base custom error and extend it further for more specific types.

// Base custom error
class AppError extends Error {
  constructor(message, code) {
    super(message);
    this.name = "AppError";
    this.code = code;
  }
}

// Specific errors extend AppError
class DatabaseError extends AppError {
  constructor(message) {
    super(message, "DB_ERROR");
    this.name = "DatabaseError";
  }
}

class TimeoutError extends AppError {
  constructor() {
    super("Request timed out", "TIMEOUT");
    this.name = "TimeoutError";
  }
}

// All are AppError and Error at the same time
let err = new DatabaseError("Connection refused");
console.log(err instanceof DatabaseError); // true
console.log(err instanceof AppError);      // true
console.log(err instanceof Error);         // true
console.log(err.code);                     // "DB_ERROR"

Diagram: Error Class Hierarchy

Error (built-in)
   │
   └─ AppError
         │
         ├─ DatabaseError
         ├─ TimeoutError
         └─ NetworkError
               │
               └─ AuthenticationError

Custom Error with Stack Trace Fix

In some older environments, the stack trace may not point to where the error was thrown. Fix this in the constructor:

class CustomError extends Error {
  constructor(message) {
    super(message);
    this.name = this.constructor.name; // auto-sets name from class name
    if (Error.captureStackTrace) {
      Error.captureStackTrace(this, this.constructor);
    }
  }
}

class PaymentError extends CustomError {}
class ShippingError extends CustomError {}

let err = new PaymentError("Card declined");
console.log(err.name);    // "PaymentError"
console.log(err.message); // "Card declined"

When to Use Custom Errors

SituationCustom Error
Form input is invalidValidationError
API returns 404NotFoundError
User not logged inAuthenticationError
Database query failsDatabaseError
Network request times outTimeoutError
Payment processing failsPaymentError

Summary

Custom errors extend the built-in Error class to create named, meaningful error types. Add extra properties like field names, status codes, or error codes to carry debugging context. Handle each error type differently with instanceof checks in catch blocks. Build a class hierarchy to group related errors under a shared base class. Custom errors make large codebases far easier to debug, test, and maintain because you always know exactly what went wrong and why.

Leave a Comment

Your email address will not be published. Required fields are marked *