skip to content

In JavaScript, a caller writes `err instanceof ValidationError` and gets false even though the code that threw called `new ValidationError(...)`. What can make that check fail, and how would you discriminate error types more robustly?

level: seniorimportance: should knowfreq 38%

answer

  1. it tests identity, not shape
  2. same source, two constructor objects
  3. separate realms have separate intrinsics
  4. strings survive what references cannot
  5. brand with a registry symbol

basics

~20 s

instanceof compares against one specific class object, so it fails whenever the thrown error was built from a different copy of that class: the defining module loaded twice, a separate realm with its own intrinsics, a downlevelled build with a broken prototype chain, or an error rebuilt after serialization.

solid answer

~50 s

`instanceof` is an identity test: it walks the object's prototype chain looking for the exact `ValidationError.prototype` object the checking code closed over. Anything that produces a second copy of that class breaks it — the defining module resolved twice in a dependency graph or bundled into two chunks, so there are two distinct constructors; an error created in another realm, where every intrinsic including `Error` is a separate object; a build downlevelled to ES5, where the subclass prototype link was never established; or an error that crossed a serialization boundary and came back as a plain object. The robust alternatives, in increasing strength: compare a documented `code` string you set yourself, export a `isValidationError(err)` type guard from the module that owns the class, or brand instances with a `Symbol.for('mylib.validationError')` key — the global symbol registry is shared across realms, so the brand survives where the constructor identity does not.

code

javascript · 24 lines
javascript
const BRAND = Symbol.for('mylib.validationError');

class ValidationError extends Error {
  constructor(message, field) {
    super(message);
    this.name = 'ValidationError';
    this.code = 'MYLIB_VALIDATION';
    this.field = field;
    this[BRAND] = true;
  }
}

function isValidationError(err) {
  return typeof err === 'object' && err !== null && err[BRAND] === true;
}

// A second, independent copy of the same class:
const Duplicate = class ValidationError extends Error {
  constructor(m) { super(m); this[Symbol.for('mylib.validationError')] = true; }
};

const fromCopy = new Duplicate('bad email');
console.log(fromCopy instanceof ValidationError); // false
console.log(isValidationError(fromCopy));         // true

go deeper

for a junior

Know that instanceof compares against one specific class object rather than matching by name, and that comparing err.name is the simple fallback when the check misbehaves.

for a middle

Explain the mechanism: instanceof walks the prototype chain for one exact prototype object, so a module evaluated twice yields two classes and neither recognises the other's instances.

for a senior

Diagnose it in a real system — duplicate copies in the dependency graph, chunk boundaries, a separate realm, a downlevelled build — and pick a discriminant that matches the boundary rather than patching the symptom at one call site.

for a principal

Own the contract a published error surface offers consumers: which discriminant is guaranteed, whether it is a code string, an exported guard or a brand, and how that promise survives repackaging, duplication and serialization over time.

## What `instanceof` actually asks `err instanceof ValidationError` does not ask "does this object look like a validation error". It walks `err`'s prototype chain and asks whether the specific object currently held in `ValidationError.prototype` appears in it. That is an identity comparison between two object references. It is exact, fast, and completely dependent on both sides referring to the *same* class object. Every failure mode below is a variation on "they don't". ## Failure mode 1 — two constructor objects for one class The most common cause in real systems is duplication. A dependency graph can resolve two versions, or even two copies of the same version, of the package that defines `ValidationError`; a bundler can inline the module into two chunks; a mixed ESM/CommonJS setup can evaluate the same file twice under two specifiers. Each evaluation creates a fresh class object with a fresh `.prototype`. ```js // Two evaluations of the same source produce unrelated classes. const A = class ValidationError extends Error {}; const B = class ValidationError extends Error {}; new A('x') instanceof B; // false — same name, different identity ``` The symptom is maddening because everything *looks* right: the name matches, the message matches, the source is one file. Only the identity differs. Module resolution and bundling decide this, which means a check that passes in one build can fail in another with no source change. ## Failure mode 2 — separate realms A realm has its own complete set of intrinsics: its own `Error`, `Array`, `Object`. An object created in one realm and passed into another carries its original prototype chain, which contains that realm's `Error.prototype`, not yours. So `err instanceof Error` itself can be false for an error that came from a different realm, and a subclass check certainly is. This is the same mechanism behind the classic `Array.isArray` existing at all — the language provides realm-independent tests precisely because `instanceof` is not one. ## Failure mode 3 — a downlevelled subclass If the class was compiled to ES5, `super` does not exist and the generated constructor calls `Error.call(this)`. The built-in `Error` ignores the `this` it is handed and returns a new object, so the instance never gets the subclass prototype. The repair is an explicit `Object.setPrototypeOf(this, new.target.prototype)` inside the constructor. Errors constructed before that line was added will fail the check. ## Failure mode 4 — the value crossed a boundary If the error was serialized and revived — sent over a network, written to a queue, put through a structured copy — what you hold is a new object built by generic machinery. It may have the right `message` and even the right `name`, but it was never constructed by your class, so no prototype link exists. ## Robust alternatives, from weakest to strongest **Compare `err.name`.** Cheap and survives all four failures, because the string travels with the data. Its weakness is that `name` is an ordinary writable string with no namespace: two independent libraries can both ship a `ValidationError`, and any code can overwrite the property. Treat it as a hint for humans and logs rather than a security-grade discriminant. **Use a documented `code` string.** A stable, namespaced constant you set in the constructor — `'MYLIB_VALIDATION'` — and publish as part of the API. It is data, so it survives serialization; it is namespaced, so collisions are your own fault; and callers can `switch` on it. This is the pattern most widely deployed error surfaces settle on. ```js class ValidationError extends Error { constructor(message, field) { super(message); this.name = 'ValidationError'; this.code = 'MYLIB_VALIDATION'; this.field = field; } } ``` **Export a type guard.** Ship `isValidationError(err)` from the module that owns the class and tell consumers to call it instead of using `instanceof`. The advantage is that the check becomes yours to define: today it can be `instanceof`, tomorrow a brand check, and no consumer has to change. **Brand with a registered symbol.** `Symbol.for('mylib.validationError')` looks the key up in the global symbol registry, which is shared across realms, so two copies of your library — even in different realms — obtain the *same* symbol. Set that key as a property in the constructor and have the guard test for it: ```js const BRAND = Symbol.for('mylib.validationError'); class ValidationError extends Error { constructor(message) { super(message); this.name = 'ValidationError'; this[BRAND] = true; } } export function isValidationError(err) { return typeof err === 'object' && err !== null && err[BRAND] === true; } ``` This survives duplicate module copies, realm boundaries, and downlevelled builds. It does not survive a plain JSON round trip — symbol keys are not serialized — which is a reminder that in-process identity and over-the-wire identity are different problems needing different fields. ## What to standardise Pick one discriminant per boundary and publish it. Inside a single module or application `instanceof` is fine and reads best. At a *published* boundary — a library others install, a plugin interface, anything that might be loaded twice — prefer an exported guard backed by a brand, and always carry a string `code` for the cases where the value has become plain data.

  • Why is a registered symbol brand more realm-robust than a class reference?
    `Symbol.for(key)` consults the global symbol registry, which the specification shares across realms rather than giving each realm its own. Two copies of your library, even loaded in different realms, therefore obtain the identical symbol value for the same key, and a property keyed by it is visible to both. A class object, by contrast, is created fresh by every evaluation, so its identity is never shared.
  • What is the weakness of discriminating on `err.name === 'ValidationError'`?
    `name` is an ordinary writable string with no namespace. Two unrelated libraries can both ship a class called `ValidationError`, so a match does not prove origin, and any code that touches the object can overwrite it. It is also easy to get wrong silently — a typo in the literal simply never matches. Use it for logs and human-facing grouping; use a namespaced code or a brand when behaviour depends on the answer.
  • Is `instanceof` ever the right choice for error discrimination?
    Yes — inside one module or one application where the class has exactly one definition and no realm boundary is crossed, it is the clearest and cheapest check, and it composes with subclass hierarchies for free. The rule of thumb is that `instanceof` is fine within a unit you build and deploy as a whole, and becomes fragile the moment the class is published for someone else to install, bundle, or load twice.
  • How do you keep the check working once errors are serialized and revived?
    Only data survives serialization, so the discriminant must be data: a stable string `code`, plus whatever structured fields the caller branches on. Symbol-keyed brands and prototype links are both lost. Define the wire shape explicitly and reconstruct a real error instance on the receiving side from that shape, rather than hoping a generic revival happens to restore the type.

saying these in an interview costs you the question

  • Says instanceof compares class names so identical names must match
  • Assumes a package can only ever be loaded once
  • Thinks err instanceof Error is always true for any thrown error
  • Treats err.name as a namespaced, tamper-proof identifier
  • Expects the prototype chain to survive JSON serialization

context