Throwing Custom Errors0%

Throwing Custom Errors

Beginner12 min read•Updated: Jul 11, 2026
Study Materials

Throwing Custom Errors in Modern JavaScript

While JavaScript provides standard error primitives (TypeError, ReferenceError), real-world enterprise applications require domain-specific errors (e.g., ValidationError, AuthenticationError, DatabaseConnectionError). Creating custom error classes allows upstream consumers to distinguish between different failure modes and handle them accordingly.


1. The throw Statement

You can throw any expression in JavaScript, but you should always throw instances of Error (or classes extending Error) to ensure stack traces are preserved:

JavaScript
// ANTI-PATTERN: Throwing raw strings or numbers loses stack traces!
// throw "Password too short";
 
// BEST PRACTICE:
throw new Error("Password must be at least 8 characters long.");

2. Creating Custom Error Classes

In modern ES6+, inherit from the built-in Error superclass:

JavaScript
class ValidationError extends Error {
constructor(message, fieldName = null) {
super(message); // Call super constructor with error message
this.name = 'ValidationError';
this.fieldName = fieldName;
this.date = new Date();
}
}
 
class InsufficientFundsError extends Error {
constructor(balance, required) {
super(`Transaction rejected: Available balance $${balance}, required $${required}`);
this.name = 'InsufficientFundsError';
this.balance = balance;
this.required = required;
}
}
Output
built-in Error
▲
│ extends
ValidationError
(custom properties: fieldName, date)

3. Differentiating Errors with instanceof

When catching errors, use instanceof to route different errors to specific recovery routines:

JavaScript
function withdrawFunds(account, amount) {
if (amount <= 0) {
throw new ValidationError('Withdrawal amount must be greater than zero.', 'amount');
}
if (account.balance < amount) {
throw new InsufficientFundsError(account.balance, amount);
}
account.balance -= amount;
return account.balance;
}
 
try {
withdrawFunds(userAccount, 500);
} catch (error) {
if (error instanceof ValidationError) {
// Highlight input field in Red on the UI
showFieldHighlight(error.fieldName, error.message);
} else if (error instanceof InsufficientFundsError) {
// Show 'Add Funds' modal
openDepositModal(error.required - error.balance);
} else {
// Unrecognized runtime crash (rethrow or send to Sentry)
console.error('Unexpected crash:', error);
throw error;
}
}

4. Error Chaining with { cause } (ES2022)

Often, a low-level error (like a disk failure or database timeout) causes a higher-level business error. ES2022 introduced the { cause } option to chain the original root error:

JavaScript
async function loadUserData(userId) {
try {
return await api.get(`/users/${userId}`);
} catch (originalNetworkError) {
// Wrap low-level error inside a domain error while preserving cause!
throw new Error(`Failed to load profile for user ${userId}`, {
cause: originalNetworkError
});
}
}
 
// In consumer:
try {
await loadUserData(42);
} catch (err) {
console.log(err.message); // 'Failed to load profile for user 42'
console.log(err.cause.message); // 'ETIMEDOUT: Connection to server timed out'
}

Interactive Quiz

Practice Quiz

Question 1
Why should you extend the built-in Error class instead of throwing plain strings?
Question 2
What keyword must be called inside the constructor of a custom class extending Error?
Question 3
How do you identify which specific custom error was caught in a catch block?
Question 4
What feature was introduced in ES2022 to preserve the original underlying error when throwing a higher-level error?
Question 5
What should you do in a catch block if the caught error is not an expected domain error?
Next Lesson

Error Handling in Async Code

Continue learning with hands-on practice, examples, and exercises in the upcoming topic.

Related Lessons

Practice Quiz

Test your understanding of this lesson with 5 questions. Each question has one correct answer.

PrevNext