skip to content

In a Node service, `err instanceof ValidationError` returns false for errors thrown by the very library that exports `ValidationError`, even though `err.constructor.name` is "ValidationError" and only one version of the library is installed. Parts of the codebase load the library with `import`, others with `require`. What is happening, and how would you confirm it?

level: seniorimportance: should knowfreq 30%

answer

  1. names match, identities do not
  2. prototype chain, not class name
  3. two builds reached by two entry paths
  4. compare require.resolve with import.meta.resolve
  5. log the module file at top level

basics

~20 s

Two builds of the same library are loaded, so two distinct ValidationError constructors exist. The error came from one copy and is being tested against the other copy's class, and instanceof compares prototype identity, not names. Confirm it by comparing the file paths each side actually resolved.

solid answer

~40 s

This is the dual package hazard. The library ships a CommonJS build and an ESM build; the `require` side of the codebase loaded one file, the `import` side loaded the other, so the class declaration was evaluated twice and there are two unrelated `ValidationError` constructors. `instanceof` walks the object's prototype chain looking for the *identical* `ValidationError.prototype` object, and the thrown error's chain contains the other copy's prototype — the matching name is a coincidence of source, not of identity. To confirm, I would compare what each side resolved: `require.resolve('pkg')` on the CommonJS side against `import.meta.resolve('pkg')` on the ESM side, and check that the two paths differ only by build file, not by version directory. `Object.getPrototypeOf(err) === ValidationError.prototype` returning false is the same signal in one line.

code

javascript · 9 lines
javascript
class ValidationError extends Error {}
const OtherCopy = class ValidationError extends Error {};

const err = new ValidationError('bad input');

console.log(err instanceof ValidationError); // true
console.log(err instanceof OtherCopy);       // false
console.log(err.constructor.name);           // "ValidationError"
console.log(Object.getPrototypeOf(err) === OtherCopy.prototype); // false

go deeper

for a junior

Know that instanceof compares against a specific prototype object, not against a class name, so two identically named classes are still different. Say that a library loaded twice yields two of everything it declares.

for a middle

Walk the mechanism end to end: two build files, two module instances, two prototype objects, one failing check. Be able to write the two-line reproduction that shows matching names with mismatched identity.

for a senior

Demonstrate the diagnostic sequence — rule out duplicate versions first, then compare the paths each side resolved, then prove double evaluation by logging the module file. Explain why the failure surfaces far from its cause in a mixed dependency graph.

for a principal

Argue about API design: identity-based contracts such as instanceof push a single-instance requirement onto every consumer's dependency graph, while structural markers degrade gracefully. Own the call on which guarantee your libraries should offer.

## The symptom and why it is confusing `instanceof` succeeding or failing feels like a question about *types*, so a false result reads as "this is a different kind of error." It is not. The check is about **object identity of a prototype**: `err instanceof C` walks `err`'s prototype chain and asks whether any link is the very same object as `C.prototype`. Two classes written identically, evaluated twice, produce two different `prototype` objects. The name on the constructor is just a string carried along for debugging, which is exactly why `err.constructor.name` still reads `"ValidationError"` while the check fails. A minimal reproduction needs no packages at all: ```js class A extends Error {} const B = class A extends Error {}; // same name, separate evaluation const e = new A('boom'); e instanceof A; // true e instanceof B; // false e.constructor.name === 'A'; // true — names match, identities do not ``` ## Why two evaluations happened here The library is dual-published: one source, two shipped build files, one selected by `require` and the other by `import`. Nothing deduplicates them, because each module cache keys on the resolved file and these are two files. So the class declaration ran twice, in two module instances, and the process now holds two `ValidationError`s. The reason it is intermittent in practice is that the two sides rarely meet directly. Your application code may be all ESM, while a transitive dependency — a framework adapter, a middleware, a test helper — still uses `require`. The error is *thrown* inside that dependency's copy and *caught* in yours. ## Confirming it, in order 1. **Rule out duplicate installs.** Two different *versions* of the package produce identical symptoms with a different cause and a different fix. Check the resolved dependency tree; if the package appears once at one version, version duplication is out. 2. **Compare what each side resolved.** On the CommonJS side, `require.resolve('pkg')` returns the absolute path actually loaded. On the ESM side, `import.meta.resolve('pkg')` returns the resolved URL (available in Node 20.6+ as a synchronous, unflagged API). Two paths inside the *same* package directory but pointing at different build files is the signature of the dual package hazard; two paths in different `node_modules` directories means duplicate installs instead. 3. **Check prototype identity directly.** `Object.getPrototypeOf(err) === ValidationError.prototype` is the raw form of the failing check; if it is false while the names match, you have two constructors. 4. **Prove the module ran twice.** A temporary top-level `console.log(import.meta.url)` — or `console.log(__filename)` in the CommonJS build — inside the installed package prints once per instantiation. Two lines with different paths is direct evidence. Inspecting the CommonJS cache keys for entries under the package directory shows the same thing from the outside. ## Fixing it at the call site versus fixing it properly As the *consumer*, the containable fixes are to stop mixing entry paths for that package (make the whole graph reach it the same way), or to stop relying on cross-copy identity — check a stable structural marker such as an error `code` string instead of `instanceof`. Both are workarounds: they treat identity as unreliable rather than restoring it. The real fix belongs to the *package*, which must guarantee that only one implementation can ever be instantiated. Until it does, any consumer that mixes entry paths hits the same wall. ## The generalisation worth stating in an interview Any API whose contract depends on identity is fragile across module copies: `instanceof`, prototype comparison, unique symbols created with `Symbol()`, `WeakMap` keys shared between modules, and singletons. APIs whose contract depends only on *shape* — a `code` property, a duck-typed method, a symbol from the global registry via `Symbol.for()` — survive duplication. When you design a library's error hierarchy, that difference decides how badly a packaging mistake can bite your users.

  • What check would you use instead of instanceof so that it keeps working even if two copies of the library are loaded?
    A structural marker the two copies agree on. A stable string property such as `err.code === 'ERR_VALIDATION'` works, and a symbol from the global registry — `err[Symbol.for('acme.validationError')] === true` — is stronger because the key cannot collide with an ordinary property. Both compare values rather than object identity, so they survive duplication, at the cost of being checkable by anyone who forges the marker.
  • The same symptom appears when two versions of the package are installed. How does the fix differ?
    Duplicate installs are a resolution problem: align the version ranges, dedupe the tree, or hoist the dependency so a single version is shared. The dual package hazard is a packaging problem inside one version, so no amount of deduping helps — the package itself must be changed to expose a single implementation, or the consumer must stop mixing entry paths.
  • Why does err.constructor.name still say "ValidationError" if the constructor is a different object?
    Because `name` is just a string property set from the class declaration when it is evaluated. Both evaluations came from the same source text, so both constructors carry the identical name string. The name tells you where the code came from, never which constructor object created the instance — which is why name comparison is a diagnostic hint, not a reliable check.

saying these in an interview costs you the question

  • instanceof failing means it is a genuinely different error type
  • Matching constructor.name proves the classes are the same
  • Reinstalling node_modules will fix it
  • instanceof compares class names at runtime
  • Wrapping the throw in try/catch changes the result

context