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.
- It usually extends the built-in Error class.
- It carries a message that explains the problem clearly.
- It can include extra properties such as an error code, status, or invalid field name.
- It helps you catch specific failures without guessing from a generic message.
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:
- you want to separate programmer mistakes from expected business-rule failures,
- you need to attach structured data to an error,
- you want different recovery paths for different failure types,
- you are building reusable code that other developers will consume.
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
- Validation failures such as missing form fields, invalid numbers, or unsupported values.
- API clients that need separate errors for network failures, invalid responses, and authorization problems.
- Domain rules such as rejecting duplicate usernames or expired coupons.
- Library code that must signal predictable, documented failure modes to consumers.
- Command-line tools that need to distinguish between user input problems and internal bugs.
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
- In older environments or some transpiled setups, custom error subclasses may need extra care to preserve instanceof behavior.
- Stack traces are usually created when the error is instantiated, so reusing the same error object can produce misleading trace information.
- Cross-realm errors, such as errors passed between iframes or different execution contexts, may fail instanceof checks even if they look identical.
- Minifiers or production builds may shorten class names, so code should not rely on constructor.name alone for logic.
- Some host environments attach extra fields to errors, such as code or cause, but those are not guaranteed to exist everywhere.
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
- Custom errors let you represent specific failure cases in JavaScript.
- They usually extend the built-in Error class.
- Use super(message) in the constructor before touching this.
- Prefer instanceof or a known error class over checking message text.
- Add extra properties when the caller needs structured information.
- Re-throw errors you do not recognize so unexpected bugs do not disappear.
11. Practice Exercise
- Create a QuotaError class that extends Error.
- Write a function called consumeQuota that throws QuotaError when the remaining quota is less than the amount requested.
- Catch the error and print both the message and the remaining quota.
- Make sure the solution uses instanceof to detect the custom error.
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.