
Learn how to extend JavaScript’s Error class correctly, build error hierarchies, and wrap exceptions for clean, scalable error handling.
Martin Ferret
March 17, 2026
JavaScript allows throwing any value, but that flexibility often leads to unstructured and fragile error handling. Strings and generic Error instances don’t scale well in real applications.
Custom errors give failures meaning. They allow you to model domain-specific problems, handle them predictably, and keep error handling readable as your codebase grows.
Error CorrectlyThe built-in Error class already provides everything you need: a message, a name, and a stack trace. Extending it properly is simple but must be done correctly.
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
}
Calling super(message) is mandatory. Without it, the error is not initialized correctly. The name property must also be set explicitly, otherwise it remains "Error".
function parseUser(json) {
const user = JSON.parse(json);
if (!user.name) {
throw new ValidationError("Missing field: name");
}
return user;
}
try {
parseUser('{ "age": 30 }');
} catch (err) {
if (err instanceof ValidationError) {
console.error(err.message);
} else {
throw err;
}
}
Using instanceof is reliable, expressive, and compatible with inheritance.
As applications grow, errors naturally form hierarchies.
class PropertyRequiredError extends ValidationError {
constructor(property) {
super(`Missing property: ${property}`);
this.name = "PropertyRequiredError";
this.property = property;
}
}
This allows you to catch:
Manually assigning name in every class becomes repetitive. A common pattern is to centralize it.
class AppError extends Error {
constructor(message) {
super(message);
this.name = this.constructor.name;
}
}
class ValidationError extends AppError {}
class PropertyRequiredError extends ValidationError {}
This keeps error classes short and consistent.
Low-level errors should not leak across abstraction boundaries. Wrapping exceptions solves this.
class ReadError extends Error {
constructor(message, cause) {
super(message);
this.name = "ReadError";
this.cause = cause;
}
}
function readUser(json) {
try {
const user = JSON.parse(json);
if (!user.name) throw new ValidationError("Missing name");
return user;
} catch (err) {
if (err instanceof SyntaxError || err instanceof ValidationError) {
throw new ReadError("Failed to read user", err);
}
throw err; // programming errors bubble up
}
}
try {
readUser("{ bad json }");
} catch (err) {
if (err instanceof ReadError) {
console.error(err.message);
console.error("Cause:", err.cause);
}
}
The caller now handles a single, meaningful error type, while still having access to the original failure.
Wrap errors when:
Do not wrap unknown or programming errors blindly. Rethrow them.
Clean error handling is not about more code.
It’s about expressing intent and maintaining control.
Extend Error properly, build clear hierarchies, and wrap exceptions when abstractions change.
Your future self, and your teammates will thank you.
Ready to prove your JavaScript skills?
Get certified in JavaScriptKeep learning JavaScript. Then get certified when you are ready.

DOM Events & Modern Patterns Cheat Sheet
Learn modern DOM event handling, including propagation, delegation, listener cleanup, custom events, forms, state management, efficient mutations, and observers to build responsive, performant pages without leaks.
Martin Ferret
Nov 10, 2026

this in JavaScript: Binding Rules and Arrow Functions
JavaScript’s this depends on the call site, with explicit binding and new taking precedence; arrow functions inherit this, making them useful for callbacks but unsuitable as object methods.
Martin Ferret
Nov 10, 2026

Iterator Helpers: Lazy Pipelines in JavaScript
ES2025 added map, filter, take and drop to iterators. Learn how lazy pipelines skip intermediate arrays, handle infinite sequences, and where they do not help.
Martin Ferret
Oct 6, 2026