JavaScript Custom Errors: Creating and Throwing Your Own Errors

Custom errors let you describe failures in your own terms instead of relying only on generic built-in errors. In JavaScript, they make debugging easier, improve API design, and help your code distinguish between different kinds of problems.

Quick answer: Create a custom error by extending Error, set a useful name and message, and throw it when your code needs to signal a specific failure. Catch it with try...catch and check its type when you want to handle it differently from other errors.

Difficulty: Intermediate

You'll understand this better if you know: basic functions, try...catch, objects, and how prototypes and classes work in JavaScript.

1. What Is Custom Errors?

A custom error is an error type you define for your own application or library. Instead of throwing only built-in errors like Error, TypeError, or RangeError, you create a named class that communicates exactly what went wrong.

For example, a form validator might throw ValidationError, while an API client might throw NetworkError or AuthenticationError.

2. Why Custom Errors Matter

Custom errors matter because large applications often need more detail than a generic error can provide. If every failure is just “Something went wrong,” it becomes harder to debug, harder to test, and harder to show the right message to the user.

They are especially useful when:

They are less useful when a built-in error already fits well. For example, TypeError is often better than inventing a new type when an argument has the wrong type.

3. Basic Syntax or Core Idea

The most common pattern is to create a class that extends Error. The constructor passes the message to super() and can set a custom name or extra fields.

Minimal custom error class

This is the smallest practical version of a custom error in modern JavaScript.

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

This code creates a new error type named ValidationError. When you throw an instance of it, the error still behaves like a normal JavaScript error, but with a more specific type and name.

Throwing the custom error

Once the class exists, you can throw it like any other error.

function validateAge(age) {
  if (age < 18) {
    throw new ValidationError("Age must be at least 18.");
  }

  return "Accepted";
}

The throw statement interrupts the function immediately, and the calling code must handle the error or let it bubble up.

4. Step-by-Step Examples

Example 1: Basic usage with try...catch

This example shows the full lifecycle: create, throw, catch, and inspect a custom error.

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

function authenticate(username, password) {
  if (username !== "admin" || password !== "secret") {
    throw new LoginError("Invalid username or password.");
  }

  return "Welcome back";
}

try {
  authenticate("guest", "1234");
} catch (error) {
  console.log(error instanceof LoginError);
  console.log(error.name);
  console.log(error.message);
}

This pattern is common in applications that need to detect a specific failure and present a different response to the user.

Example 2: Adding extra data to the error

You can attach structured information to a custom error, which is helpful for validation and APIs.

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

function requireEmail(email) {
  if (!email || !email.includes("@")) {
    throw new ValidationError("Email must contain @.", "email");
  }
}

try {
  requireEmail("example.com");
} catch (error) {
  console.log(error.field);
}

The extra field property makes it easier to highlight the exact input that failed.

Example 3: Handling different error types differently

When code may fail for multiple reasons, you can branch based on the error type.

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

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

function loadUserData() {
  throw new NetworkError("Server did not respond.");
}

try {
  loadUserData();
} catch (error) {
  if (error instanceof NetworkError) {
    console.log("Show retry button");
  } else if (error instanceof ParseError) {
    console.log("Show invalid data message");
  } else {
    throw error;
  }
}

Using distinct classes lets the catch block make a precise decision instead of relying on message text.

Example 4: Custom errors in a reusable helper

Library-style code often uses custom errors so callers can handle predictable failures cleanly.

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

function createClient(options) {
  if (!options.apiKey) {
    throw new ConfigError("apiKey is required.");
  }

  return {
    apiKey: options.apiKey
  };
}

This is useful when your function has strict setup requirements and you want the caller to know exactly what was missing.

5. Practical Use Cases

Custom errors are most valuable when the caller can do something meaningful with the difference between one error and another.

6. Common Mistakes

Mistake 1: Throwing a plain object instead of an Error instance

Some beginners throw an object with message and name properties. That can work in a limited sense, but it does not behave like a real error and often breaks stack traces and type checks.

Problem: The thrown value is not an Error instance, so debugging tools and instanceof checks do not behave as expected.

throw {
  name: "ValidationError",
  message: "Age must be at least 18."
};

Fix: Extend Error and throw an instance of your custom class.

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

throw new ValidationError("Age must be at least 18.");

The corrected version works because it creates a genuine error object with the proper prototype chain.

Mistake 2: Forgetting to call super()

In a subclass of Error, you must call super() before using this. If you skip it, the class cannot initialize properly.

Problem: JavaScript throws a runtime error such as ReferenceError: Must call super constructor in derived class before accessing 'this'.

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

Fix: Call super(message) first, then set any extra properties.

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

This works because the parent Error constructor creates the underlying error state first.

Mistake 3: Relying on error message text instead of type

Comparing error.message is fragile because messages can change during refactoring, localization, or dependency updates.

Problem: Message-based checks make error handling brittle and can fail silently when text changes.

try {
  saveProfile();
} catch (error) {
  if (error.message === "Invalid email") {
    console.log("Show email field error");
  }
}

Fix: Use a custom error class and check with instanceof.

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

try {
  saveProfile();
} catch (error) {
  if (error instanceof ValidationError) {
    console.log("Show email field error");
  }
}

The corrected version is easier to maintain because the error type stays stable even if the message changes.

7. Best Practices

Practice 1: Use clear, specific names

Choose names that describe the problem domain, not the implementation detail. A name like PaymentDeclinedError is more useful than Error1 or BadThingError.

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

Specific names make logs, tests, and catch blocks much easier to understand.

Practice 2: Add structured properties for recovery

If the caller needs to react to the error, give it data that helps. This is better than forcing the caller to parse a sentence.

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

A status code or field name gives the caller a stable way to decide what to do next.

Practice 3: Re-throw unknown errors

When you catch errors, handle only what you recognize and re-throw the rest. This prevents your code from hiding unexpected bugs.

try {
  doWork();
} catch (error) {
  if (error instanceof ValidationError) {
    console.log("Fix user input");
  } else {
    throw error;
  }
}

This pattern keeps genuine bugs visible instead of accidentally swallowing them.

8. Limitations and Edge Cases

If you need to chain errors, JavaScript also supports an optional cause property in modern runtimes, which helps preserve the original failure when wrapping it in a new custom error.

9. Practical Mini Project

Here is a small validation helper for a signup form. It throws custom errors for specific invalid inputs and catches them in one place.

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

function validateSignup(data) {
  if (!data.email) {
    throw new ValidationError("Email is required.", "email");
  }

  if (!data.email.includes("@")) {
    throw new ValidationError("Email is invalid.", "email");
  }

  if (!data.password || data.password.length < 8) {
    throw new ValidationError("Password must be at least 8 characters.", "password");
  }

  return true;
}

try {
  validateSignup({
    email: "sam.example.com",
    password: "123"
  });
  console.log("Signup is valid");
} catch (error) {
  if (error instanceof ValidationError) {
    console.log("Field:", error.field);
    console.log("Message:", error.message);
  } else {
    console.error("Unexpected error:", error);
  }
}

This example shows how a custom error can carry both a human-readable message and machine-friendly context. The calling code can show the right field message without guessing what failed.

10. Key Points

11. Practice Exercise

Expected output: The program should report that quota is too low and show the remaining amount.

Hint: Store the remaining quota as a property on the error instance so the catch block can read it.

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

function consumeQuota(remaining, amount) {
  if (remaining < amount) {
    throw new QuotaError("Not enough quota available.", remaining);
  }

  return remaining - amount;
}

try {
  consumeQuota(3, 5);
} catch (error) {
  if (error instanceof QuotaError) {
    console.log(error.message);
    console.log("Remaining:", error.remaining);
  }
}

12. Final Summary

Custom errors are a clean way to make JavaScript error handling more expressive. Instead of throwing a generic failure, you can communicate the exact kind of problem your code encountered, which makes debugging and recovery much easier.

The core pattern is simple: extend Error, call super(message), add any useful properties, and throw the instance when a specific condition fails. From there, use try...catch and instanceof to handle known problems without hiding unexpected ones.

If you want to go further, the next useful topic is JavaScript error handling patterns such as re-throwing, wrapping errors with cause, and deciding when to use built-in error types versus your own custom classes.