The built-in Error class is fine for generic failures, but in a real application you often want to distinguish kinds of errors — a validation failure is different from a network failure, which is different from a "user not found" failure. JavaScript lets you create your own error types by subclassing Error, so calling code can check exactly what went wrong instead of parsing a message string.

Creating a Custom Error Class

class ValidationError extends Error {
  constructor(message, field) {
    super(message);       // sets this.message
    this.name = "ValidationError";
    this.field = field;   // extra data specific to this error type
  }
}

function validateAge(age) {
  if (age < 0) {
    throw new ValidationError("Age cannot be negative", "age");
  }
  return age;
}

try {
  validateAge(-5);
} catch (error) {
  console.log(error.name);    // ValidationError
  console.log(error.message); // Age cannot be negative
  console.log(error.field);   // age
}

Output:

ValidationError
Age cannot be negative
age

The key line is super(message) — it calls the parent Error constructor, which sets up this.message and the stack trace correctly. Skipping it would leave your custom error missing standard behavior that other code (and tools like loggers) expect from any Error.

Checking the Error Type

Because ValidationError really is a subclass of Error, you can use instanceof to branch on it — this is the main reason to bother creating a custom error class at all:

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

function handleError(error) {
  if (error instanceof ValidationError) {
    console.log(`Fix your input: ${error.field}`);
  } else if (error instanceof NotFoundError) {
    console.log("That resource doesn't exist.");
  } else {
    console.log("Unexpected error:", error.message);
  }
}

handleError(new NotFoundError("User 42 not found"));

Output:

That resource doesn't exist.

When Is a Custom Error Worth It?

For a small script, the built-in Error is usually enough — don't reach for custom error classes by default. They start paying off once your code has multiple distinct failure modes that calling code needs to react to differently (retry a network error, show a form message for a validation error, redirect on an auth error), or once you're building something other code will consume, like a library or an API layer, where callers benefit from catching specific, documented error types rather than string-matching a generic message.

Custom errors pair naturally with try...catch and with Promise rejections — a rejected Promise can reject with any value, including one of your own error instances, and it will be caught the same way in a .catch() or an await wrapped in try...catch.