Sometimes the built-in errors are too general. A custom error has its own name and can carry extra information, so callers can react to specific problems.
class ValidationError extends Error {
constructor(message, field) {
super(message); // sets the message
this.name = "ValidationError";
this.field = field;
}
}
function validateUser(user) {
if (!user.email) {
throw new ValidationError("Email is required", "email");
}
return true;
}
try {
validateUser({});
} catch (error) {
console.log(error.name); // → ValidationError
console.log(error.message); // → Email is required
console.log(error.field); // → email
}
class ... extends Error builds on classes. Calling super(message) is required before you use this.
Telling errors apart
instanceof lets you handle each kind differently.
class NotFoundError extends Error {
constructor(resource) {
super(`${resource} was not found`);
this.name = "NotFoundError";
}
}
class PermissionError extends Error {
constructor() {
super("You do not have permission");
this.name = "PermissionError";
}
}
function load(kind) {
if (kind === "missing") throw new NotFoundError("User");
if (kind === "private") throw new PermissionError();
return "data";
}
for (const kind of ["ok", "missing", "private"]) {
try {
console.log(load(kind));
} catch (error) {
if (error instanceof NotFoundError) {
console.log("404 page:", error.message);
} else if (error instanceof PermissionError) {
console.log("show login:", error.message);
} else {
throw error; // unknown error: do not hide it
}
}
}
The final else { throw error; } is important: only handle errors you recognise, and rethrow the rest.
A practical example: HTTP errors
class HttpError extends Error {
constructor(status, message = `HTTP ${status}`) {
super(message);
this.name = "HttpError";
this.status = status;
}
}
// later
if (!response.ok) throw new HttpError(response.status);
You saw this idea in A Practical fetch() Helper.
A small base class
When you have several, share the boilerplate:
class AppError extends Error {
constructor(message, options) {
super(message, options);
this.name = this.constructor.name; // uses the class name
}
}
class DatabaseError extends AppError {}
const error = new DatabaseError("Connection lost");
console.log(error.name); // → DatabaseError
console.log(error instanceof AppError); // → true
console.log(error instanceof Error); // → true
When to bother
- You need to distinguish between failures in
catch. - You want to attach data (field, status code, id).
- A library or shared module should expose clear failure types.
For small scripts, plain new Error("message") is enough. Custom errors are a tool for code that grows.