Skip to content
JavaScript js error-handling 4 min read

Custom Error Classes

Throwing plain Error objects works, but as an application grows you often need to distinguish what kind of failure occurred — a validation problem versus a missing record versus a network timeout. Custom error classes let you attach a meaningful name, carry structured data, and catch specific failures by type. Because JavaScript’s Error is just a class, you extend it like any other, gaining instanceof checks and clean, self-documenting failure modes.

Extending the Error class

A custom error is any class that extends the built-in Error. Inside the constructor you call super(message) to set the standard message property, then assign a unique name. Setting name is what makes the error print as ValidationError: ... instead of the generic Error: ... in stack traces and logs.

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

try {
  throw new ValidationError("Email is required");
} catch (err) {
  console.log(err.name);
  console.log(err.message);
  console.log(err instanceof ValidationError);
  console.log(err instanceof Error);
}

Output:

ValidationError
Email is required
true
true

Notice that the instance is both a ValidationError and an Error. That dual identity is the whole point: callers who don’t care about specifics can still catch it as a generic Error, while callers who do care can branch on the precise type.

Always set this.name in the constructor. Without it the name stays "Error", which makes logs and monitoring dashboards much harder to scan.

Adding context and the cause property

Real errors carry more than a string. Add your own properties to expose structured details — a field name, an HTTP status, a record id — so the catch site can react programmatically instead of parsing the message. ES2022 also standardized the cause option, letting you wrap a lower-level error while preserving the original.

class NotFoundError extends Error {
  constructor(resource, id, options) {
    super(`${resource} with id ${id} not found`, options);
    this.name = "NotFoundError";
    this.resource = resource;
    this.id = id;
  }
}

async function loadUser(id) {
  try {
    const res = await fetch(`/api/users/${id}`);
    if (res.status === 404) {
      throw new NotFoundError("User", id);
    }
    return await res.json();
  } catch (networkErr) {
    // Re-wrap an unexpected failure while keeping the original
    throw new NotFoundError("User", id, { cause: networkErr });
  }
}

The cause is accessible as err.cause, and modern runtimes (Node 16.9+, all current browsers) print it in the stack trace automatically, so you never lose the root failure.

Building an error hierarchy

For larger codebases, define a shared base class and extend it. This gives you one type to catch all of your application’s errors, plus granular subclasses for specific handling. A common pattern is to attach an HTTP status or error code on the base so middleware can translate any of them into a response.

class AppError extends Error {
  constructor(message, options) {
    super(message, options);
    this.name = this.constructor.name; // auto-names every subclass
    this.timestamp = new Date().toISOString();
  }
}

class ValidationError extends AppError {
  constructor(message, field) {
    super(message);
    this.field = field;
    this.status = 400;
  }
}

class NotFoundError extends AppError {
  constructor(message) {
    super(message);
    this.status = 404;
  }
}

Using this.constructor.name means each subclass is named correctly without repeating a string literal in every constructor.

Catching by type

The payoff arrives at the catch site. Use instanceof to branch on the kind of error, handling known failures gracefully and re-throwing anything unexpected so it surfaces instead of being silently swallowed.

function handleRequest(action) {
  try {
    action();
  } catch (err) {
    if (err instanceof ValidationError) {
      console.log(`400 Bad Request — field "${err.field}": ${err.message}`);
    } else if (err instanceof NotFoundError) {
      console.log(`404 Not Found — ${err.message}`);
    } else if (err instanceof AppError) {
      console.log(`Application error — ${err.message}`);
    } else {
      throw err; // unknown: let it propagate
    }
  }
}

handleRequest(() => {
  throw new ValidationError("Must be a valid email", "email");
});
handleRequest(() => {
  throw new NotFoundError("Order #42 does not exist");
});

Output:

400 Bad Request — field "email": Must be a valid email
404 Not Found — Order #42 does not exist

Order your instanceof checks from most specific to least specific. Because subclasses also match their parent, putting instanceof AppError first would swallow ValidationError before its dedicated branch runs.

Property reference

PropertySourcePurpose
messagesuper(message)Human-readable description
nameset manuallyIdentifies the error type in logs and stacks
stackautomaticCaptured call stack at construction
cause{ cause } option (ES2022)The underlying error being wrapped
custom (e.g. field, status)your constructorStructured data for programmatic handling

Best Practices

  • Always extend Error (or a subclass of it) so instanceof Error and stack keep working as expected.
  • Set this.name — use this.constructor.name in a base class to auto-name every subclass.
  • Attach structured fields (status, code, field) instead of encoding data inside the message string.
  • Use the { cause } option to wrap lower-level errors rather than discarding them.
  • Define a single base class (e.g. AppError) so you can catch all of your errors with one instanceof.
  • Check instanceof from most specific to least specific, and re-throw unknown errors rather than swallowing them.
Last updated June 1, 2026
Was this helpful?