JavaScript Custom Errors
A custom error is a class you define that extends JavaScript’s built-in Error, so you can throw objects that carry your own name, extra data, and behavior while still working with every tool built for standard errors — try/catch, instanceof, stack traces, and error-logging libraries. Instead of throwing a plain string or a generic Error with a vague message, custom errors let calling code distinguish ‘the network failed’ from ‘the input was invalid’ from ‘the user isn’t allowed to do this’, and react to each differently.
Overview / How It Works
Every error thrown in JavaScript is, at minimum, an object with a message, a name, and a stack trace — that is the contract the Error constructor sets up. When you write class MyError extends Error, you are creating a subclass: MyError.prototype sits below Error.prototype in the prototype chain, so every instance of MyError is also, genuinely, an instance of Error. Inside the constructor you must call super(message) before you touch this, because in a derived class the engine does not allocate this until the parent constructor — Error‘s — has run and initialized it. That single call to super(message) is what sets the inherited message property and generates the stack property.
Why subclass instead of just doing const err = new Error('bad input'); err.field = 'email';? Two reasons. First, discoverability and type-safety: error instanceof ValidationError is far more reliable in a catch block than checking whether err.field is defined or parsing err.message. Second, reuse: a class bundles the shape of the error together with its construction logic, so every ValidationError anywhere in a codebase is guaranteed to have the same fields, created the same way. This becomes essential once an application needs to distinguish more than one or two kinds of failure — invalid input, missing resource, permission denied, network timeout — each deserving its own handling path.
Under the hood, class Foo extends Error {} is sugar over prototype-based inheritance: Foo.prototype‘s internal prototype is Error.prototype, and Foo.prototype.constructor is Foo. When new Foo('msg') runs, the engine creates an object whose internal prototype is Foo.prototype, then runs Foo‘s constructor; the super(message) call inside it runs Error‘s constructor with this already bound to the new object, setting this.message and capturing this.stack. Because assignments like this.name = ... happen after super() returns, custom properties become ordinary own properties of the instance — enumerable and visible to Object.keys(), unlike message and stack, which are non-enumerable on the base Error.
Syntax
class CustomError extends Error {
constructor(message, extraInfo) {
super(message); // sets this.message and this.stack
this.name = 'CustomError'; // shows up in toString() and stack traces
this.extraInfo = extraInfo; // any extra data you want to carry
}
}
// throw new CustomError('Something specific went wrong', { code: 42 });
extends Error— putsCustomError.prototypein front ofError.prototypein the chain, soinstanceof Errorstill works.constructor(message, ...)— accept whatever arguments make sense;messageis conventional but not required.super(message)— must run beforethisis used; delegates toError‘s own constructor.this.name— override the inherited'Error'name so logs andtoString()identify the specific error type.- extra properties — attach structured data instead of encoding it into the message string.
Examples
Example 1: A basic custom error
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = 'ValidationError';
}
}
function validateAge(age) {
if (age < 0) {
throw new ValidationError('Age cannot be negative');
}
return age;
}
try {
validateAge(-5);
} catch (error) {
console.log(error.name);
console.log(error.message);
console.log(error instanceof ValidationError);
console.log(error instanceof Error);
}
Output:
ValidationError
Age cannot be negative
true
true
ValidationError only overrides name; everything else — message, the prototype chain, stack — comes free from Error. Because the chain includes Error.prototype, the caught object is instanceof both ValidationError and Error, so generic error-handling code and specific error-handling code can both recognize it.
Example 2: Attaching structured data
class HttpError extends Error {
constructor(message, statusCode, url) {
super(message);
this.name = 'HttpError';
this.statusCode = statusCode;
this.url = url;
}
}
function fetchResource(url, statusCode) {
if (statusCode >= 400) {
throw new HttpError(`Request failed with status ${statusCode}`, statusCode, url);
}
return { url, statusCode };
}
try {
fetchResource('/api/users/42', 404);
} catch (error) {
if (error instanceof HttpError) {
console.log(`${error.name}: ${error.message}`);
console.log(`Status: ${error.statusCode}, URL: ${error.url}`);
}
}
Output:
HttpError: Request failed with status 404
Status: 404, URL: /api/users/42
statusCode and url are real properties on the error object, not text buried inside the message. Calling code can branch on error.statusCode >= 500 to decide whether a retry makes sense, or log error.url separately from the human-readable message — something that is awkward when every detail is squeezed into one string.
Example 3: An error hierarchy
class AppError extends Error {
constructor(message) {
super(message);
this.name = this.constructor.name;
}
}
class NotFoundError extends AppError {
constructor(resource) {
super(`${resource} was not found`);
this.resource = resource;
}
}
class PermissionError extends AppError {
constructor(action) {
super(`You do not have permission to ${action}`);
this.action = action;
}
}
function handle(error) {
if (error instanceof NotFoundError) {
console.log(`404 handler: ${error.message}`);
} else if (error instanceof PermissionError) {
console.log(`403 handler: ${error.message}`);
} else if (error instanceof AppError) {
console.log(`Generic app error: ${error.message}`);
} else {
throw error;
}
}
handle(new NotFoundError('User'));
handle(new PermissionError('delete this post'));
Output:
404 handler: User was not found
403 handler: You do not have permission to delete this post
AppError acts as a shared base class; NotFoundError and PermissionError extend it and add their own data. handle checks from most specific to least specific with instanceof, and rethrows anything it does not recognize — the correct pattern for a catch-all that should not silently swallow unrelated bugs. this.constructor.name automatically picks up the subclass’s actual name without repeating a string in every class.
Under the Hood: The Prototype Chain
When JavaScript executes new DatabaseError('Connection refused'), four things happen in order:
- The engine allocates a new object and privately links its prototype to
DatabaseError.prototype— not yet usable asthisinside the constructor. super(message)runs theErrorconstructor with that object asthis: it sets the message property and prepares astackstring describing the current call site.- Control returns to
DatabaseError‘s constructor, wherethisis now usable;this.name = 'DatabaseError'runs as an ordinary own-property write. - The finished object is returned. Its prototype chain is
DatabaseError.prototype → Error.prototype → Object.prototype, exactly whatinstanceofwalks when checkingerr instanceof Error.
class DatabaseError extends Error {
constructor(message) {
super(message);
this.name = 'DatabaseError';
}
}
const err = new DatabaseError('Connection refused');
console.log(Object.getPrototypeOf(err) === DatabaseError.prototype);
console.log(Object.getPrototypeOf(DatabaseError.prototype) === Error.prototype);
console.log(typeof err.stack);
console.log(err.stack.split('\n')[0]);
Output:
true
true
string
DatabaseError: Connection refused
err.stack is formatted lazily by the engine the first time it is read, using whatever this.name and this.message are at that moment — which is why setting this.name inside the constructor, before the object is ever inspected, reliably shows up as the first line of the trace even though super() technically ran before that assignment.
Common Mistakes
Mistake 1: Using this before calling super()
In a derived class, this does not exist until the parent constructor runs. Code like class BadError extends Error { constructor(message) { this.message = message; } } throws a ReferenceError: Must call super constructor before accessing 'this' the instant you write new BadError('x') — it never even gets to create the error you meant to throw. Always call super(...) as the very first line of the constructor:
class ParseError extends Error {
constructor(message, line) {
super(message);
this.name = 'ParseError';
this.line = line;
}
}
try {
throw new ParseError('Unexpected token', 12);
} catch (error) {
console.log(`${error.name} on line ${error.line}: ${error.message}`);
}
Output:
ParseError on line 12: Unexpected token
With super(message) called first, this is fully initialized before the constructor adds name and line, so the object behaves like a normal error plus the extra data.
Mistake 2: Forgetting to set this.name
Without it, every custom error still identifies itself as plain 'Error' in logs, stack traces, and toString(), which defeats the purpose of giving it a distinct type:
class TimeoutError extends Error {}
const withoutName = new TimeoutError('Request timed out');
console.log(withoutName.name);
console.log(withoutName.toString());
class FixedTimeoutError extends Error {
constructor(message) {
super(message);
this.name = 'FixedTimeoutError';
}
}
const withName = new FixedTimeoutError('Request timed out');
console.log(withName.name);
console.log(withName.toString());
Output:
Error
Error: Request timed out
FixedTimeoutError
FixedTimeoutError: Request timed out
TimeoutError has no constructor of its own, so it uses the default derived constructor, which forwards arguments to super(...args) but never touches name — leaving it inherited from Error.prototype. toString() builds its string as ${this.name}: ${this.message}, so an unset name silently produces misleading output in every log line.
Mistake 3: Branching on error.message text
Message strings are meant for humans and change wording, punctuation, or language over time — matching them with something like error.message.includes('not found') is brittle and breaks silently the moment someone rewords the message. Give every error type its own class (or a stable, machine-readable code property) and branch with instanceof instead, as shown in the AppError hierarchy above.
Best Practices
- Always call
super(message)as the first statement in the constructor, before touchingthis. - Set
this.nameto the class name (orthis.constructor.namein a shared base class) so logs andtoString()show the real error type. - Build a small hierarchy — one base class like
AppError, with specific subclasses per failure mode — instead of one giant error class with a type string field. - Attach structured data (
statusCode,field,resource) as real properties, not text baked into the message. - When wrapping a lower-level error, use the standard
causeoption —super(message, { cause: originalError })— so the original stack is not lost. - Prefer
instanceofor a dedicatedcodeproperty over parsingerror.messagestrings. - Only catch errors you can meaningfully handle or translate; rethrow anything else so bugs are not silently swallowed.
- Remember that
messageandstackare non-enumerable — if you serialize errors for logging, add atoJSON()method or spread the fields explicitly.
Practice Exercises
- Create an
InsufficientFundsErrorthat extendsErrorand stores ashortfallproperty. Write awithdraw(balance, amount)function that throws it whenamountexceedsbalance, then catch it and print a message such as ‘Cannot withdraw: short by $50’. - Build a two-level hierarchy: a base
ApiErrorclass and two subclasses,BadRequestError(400) andUnauthorizedError(401), each setting a numericstatusCode. Write a function that receives an error and logs a different message per subclass usinginstanceof, with a generic fallback for anything else. - Write a function
loadConfig()that throws a plainError('file not found'). Call it insidetry/catch, and in the catch block throw a newConfigErrorthat preserves the original error using thecauseoption. Log botherror.messageanderror.cause.message.
Summary
- A custom error is a class that extends
Error, gainingmessage,stack, and full compatibility withinstanceofandtry/catch. super(message)must run beforethisis used, because the parent constructor initializes the instance.- Set
this.nameexplicitly — otherwise stack traces andtoString()still report plain'Error'. - Extra properties on a custom error turn unstructured text into structured, programmatically usable data.
- Prototype-chain subclassing is why
instanceof Errorkeeps working for every custom error type. - Build hierarchies — a shared base class plus specific subclasses — for layered, precise error handling.
- Use the
causeoption to chain errors without discarding the original failure’s context.
