skip to content

In JavaScript, given `class ValidationError extends Error {}`, the expression `new ValidationError('bad email').name` evaluates to "Error" rather than "ValidationError". Why, and what does a correctly written custom error class set?

level: middleimportance: must knowfreq 62%

answer

  1. name is inherited, not generated
  2. super(message) covers only half of it
  3. assign the name in the constructor
  4. new.target.name survives further subclassing
  5. downlevelled classes lose the prototype link

basics

~20 s

The name property is inherited from Error.prototype, whose value is the string Error; an empty subclass overrides nothing. A correct custom error calls super(message) and then assigns its own name, plus whatever structured fields callers need.

solid answer

~40 s

`name` is not derived from the class — it is a plain string property that lives on `Error.prototype`, and each built-in subtype shadows it on its own prototype. An empty `extends Error` adds nothing, so the lookup walks straight through to `"Error"`. `super(message)` handles the message half correctly; the name half is yours. So write a constructor that calls `super(message)` and then sets `this.name = 'ValidationError'` — or `this.name = new.target.name`, which keeps further subclasses correct — and attach the fields callers actually branch on, such as a `field` or a stable `code`. One build-time caveat: if the class is downlevelled to ES5, the prototype chain is rebuilt incorrectly and `instanceof` on the subclass can fail, which is why generated code inserts `Object.setPrototypeOf(this, new.target.prototype)`.

code

javascript · 14 lines
javascript
class ValidationError extends Error {
  constructor(message, field) {
    super(message);
    this.name = 'ValidationError';
    this.field = field;
  }
}

const err = new ValidationError('must be an email', 'contactEmail');
console.log(err.name);      // "ValidationError"
console.log(err.message);   // "must be an email"
console.log(String(err));   // "ValidationError: must be an email"
console.log(err.field);     // "contactEmail"
console.log(err instanceof ValidationError, err instanceof Error); // true true

go deeper

for a junior

Be able to write a custom error that calls super(message) and assigns this.name, and to say why the class name does not appear on the instance by itself.

for a middle

Explain that name is resolved through the prototype chain from Error.prototype, what Error.prototype.toString builds out of name and message, and the difference between assigning name on the instance versus the prototype.

for a senior

Show that you design the payload, not just the label: stable fields callers can branch on, a constructor that cannot throw, and awareness that identity of the class can be broken by the build target or a duplicated module.

for a principal

Own the taxonomy question — how many error classes a codebase should have, what shape they share so logging and telemetry can group them, and how that contract stays stable as the code is packaged and consumed.

## What `extends Error` gives you, and what it doesn't With a native ES2015+ class, `class ValidationError extends Error {}` already does more than people assume. The implicit derived constructor forwards its arguments to `super`, so `new ValidationError('bad email').message` is `'bad email'`. The instance's prototype is `ValidationError.prototype`, so `instanceof` works for both the subclass and `Error`. Engines attach their usual diagnostic properties. What it does *not* do is invent a `name`. ## Where `name` actually lives `name` is an ordinary string data property. `Error.prototype.name` is `"Error"`; `TypeError.prototype.name` is `"TypeError"`, and so on — the built-in subtypes each shadow it deliberately. Your subclass's prototype object has no `name` of its own, so a property lookup on the instance walks `instance → ValidationError.prototype → Error.prototype` and finds `"Error"`. ```js class ValidationError extends Error {} const e = new ValidationError('bad email'); e.message; // "bad email" e.name; // "Error" Object.hasOwn(ValidationError.prototype, 'name'); // false e instanceof ValidationError; // true ``` The class *does* have a name — `ValidationError.name` is `"ValidationError"` — but that is the function's own `name` property, and nothing copies it onto instances. ## Why it matters beyond cosmetics `Error.prototype.toString()` is `name + ": " + message`, and that is what you see when the error is interpolated into a string, concatenated into a log line, or printed by a top-level handler. Leave `name` unset and every custom error in your logs is labelled `Error: ...`, erasing exactly the distinction you created the class to express. Code that discriminates on `err.name` — a common pattern at package boundaries — gets the wrong answer too. ## Three ways to set it ```js // 1. On the instance, in the constructor. class ValidationError extends Error { constructor(message, field) { super(message); this.name = 'ValidationError'; this.field = field; } } // 2. On the prototype, once. class ParseError extends Error {} ParseError.prototype.name = 'ParseError'; // 3. From new.target, so subclasses stay correct. class AppError extends Error { constructor(message) { super(message); this.name = new.target.name; } } class NotFoundError extends AppError {} new NotFoundError('missing').name; // "NotFoundError" ``` Option 3 works because `new.target` inside the constructor is the constructor that was actually invoked with `new`, and a derived class with no explicit constructor forwards it through. Options 1 and 2 differ in a detail worth knowing: an assignment in the constructor creates an *own, enumerable* property on every instance, so `name` shows up in `Object.keys(err)` and in a `JSON.stringify` of the error, whereas the prototype assignment keeps instances clean (though a `for...in` still walks up and sees it). Built-in errors keep `name` non-enumerable on the prototype; the prototype form is the closer match. ## Carrying data, not just a message The real value of a custom error class is the structured payload. A message is for humans; the fields are for code. Attach the identifiers a caller needs to react — the failing field, an operation id, a stable string `code` — and keep them own enumerable properties so they survive being logged. ```js class HttpError extends Error { constructor(status, url) { super(`Request to ${url} failed with ${status}`); this.name = 'HttpError'; this.status = status; this.url = url; } } ``` Keep the constructor cheap and total: it should not perform I/O, and it must not throw, because it runs on the failure path where things are already going wrong. ## The serialization surprise `JSON.stringify(new Error('boom'))` produces `"{}"`. `message` is an own property but **non-enumerable**, and `name` is not an own property at all, so the default serializer finds nothing. Fields you assign yourself *are* enumerable and do survive, which means a custom error partially serializes while a built-in one vanishes. If errors travel as JSON, define an explicit shape rather than relying on `stringify`. ## Why `instanceof` can break in a downlevelled build When a toolchain compiles the class down to ES5 function syntax, it cannot use `super` — it calls `Error.call(this, message)`. The built-in `Error` constructor called that way ignores the passed `this` and returns a fresh object, so the instance under construction never gets the right prototype and `err instanceof ValidationError` can come back `false`. The standard repair inside the constructor is: ```js Object.setPrototypeOf(this, new.target.prototype); ``` This is unnecessary — and harmless — when the class runs as a native class, which is the normal case on an ES2015+ target. Knowing why the line exists is the point: it tells you that error-class identity is a build-target concern, not only a source-code one.

  • Why does `JSON.stringify(new Error('boom'))` produce `{}`?
    `JSON.stringify` only serializes own enumerable properties. On a built-in error, `message` is an own property but non-enumerable, and `name` is not an own property at all — it lives on the prototype. So the serializer finds nothing to write. Fields you assign yourself in a custom error's constructor are enumerable and do survive, which is why a custom error partially serializes while a plain one disappears entirely.
  • Is it better to set name on the instance or on the prototype?
    The prototype form (`MyError.prototype.name = 'MyError'`) matches the built-ins more closely: instances stay free of an own `name`, so it does not appear in `Object.keys` or in a `JSON.stringify` of the error. Assigning `this.name` in the constructor creates an own enumerable property on every instance — slightly heavier, but it makes the type visible in serialized logs, which many teams consider a feature.
  • Why do generated custom error classes contain `Object.setPrototypeOf(this, new.target.prototype)`?
    It repairs a downlevelled build. Compiled to ES5 there is no `super`, so the generated constructor calls `Error.call(this, message)`; the built-in Error constructor ignores the `this` it is handed and returns a fresh object, leaving the instance with the wrong prototype and breaking `instanceof` on the subclass. Resetting the prototype from `new.target` restores it. On a native ES2015+ class it is unnecessary.

saying these in an interview costs you the question

  • Assumes extends Error sets name to the class name automatically
  • Uses this before calling super() in the derived constructor
  • Believes instanceof for custom errors works on every build target
  • Treats JSON.stringify as a reliable way to log an error object
  • Puts the only useful detail in the message string instead of fields

context