Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

JavaScript (ES2026)  Module 11 – Prototypes and classes

instanceof, duck typing and cross-realm checks

Why instanceof walks the prototype chain and fails for values from iframes or vm contexts, which checks work across realms, and where duck typing fits.

  • Advanced
  • 20 minutes
  • Examples run with Node.js 24.21.0 and quickjs 0.32.0
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Check types with instanceof, Array.isArray, Error.isError and duck typing
  • Explain why instanceof fails for values made in another realm, such as an iframe or a vm context
  • Choose checks that give the same answer across realms and across two copies of a library
  • Use Symbol.hasInstance to change what instanceof answers, and recognise when not to

Before you start

What you’ll learn · Before you start

What you will learn

  • Check types with instanceof, Array.isArray, Error.isError and duck typing
  • Explain why instanceof fails for values made in another realm, such as an iframe or a vm context
  • Choose checks that give the same answer across realms and across two copies of a library
  • Use Symbol.hasInstance to change what instanceof answers, and recognise when not to

Before you start

On this page

JavaScript has several ways to ask “what is this value?”, and each answers a different question. instanceof walks the value’s prototype chain, looking for a constructor’s prototype object. That is quick and usually right, but it fails for values made in another realm: every window, iframe, worker and Node.js vm context has its own set of built-ins, so an array made in an iframe is not instanceof the parent page’s Array. Array.isArray and, since ECMAScript 2026, Error.isError look at the object itself and work across realms. Duck typing checks what a value can do instead of what made it, such as having a callable then or Symbol.iterator; it is how await recognises promises and for…of recognises iterables. And Symbol.hasInstance lets a class decide what instanceof answers. This lesson shows what each check really asks, reproduces the cross-realm failure in a second realm, and chooses checks that survive iframes and two copies of the same library.

What instanceof really checks

walkChain below does what instanceof does for an ordinary class, and the two agree on every case:

instanceof next to a hand-written walk of the prototype chain JavaScript · instanceof-walk.mjs
// What `value instanceof C` does for an ordinary class: walk value's prototype chain, looking for C.prototype.
function walkChain(value, C) {
  if (typeof value !== 'object' && typeof value !== 'function') return false; // primitives have no chain
  if (value === null) return false;
  for (let proto = Object.getPrototypeOf(value); proto !== null; proto = Object.getPrototypeOf(proto)) {
    if (proto === C.prototype) return true;
  }
  return false;
}

class Vehicle {}
class Bus extends Vehicle {}
const bus = new Bus();
const lookalike = Object.create(Bus.prototype); // never passed through a constructor
const dictionary = Object.create(null); // no prototype at all

const cases = [
  ['bus, Bus', bus, Bus],
  ['bus, Vehicle', bus, Vehicle],
  ['bus, Object', bus, Object],
  ['lookalike, Bus', lookalike, Bus],
  ['dictionary, Object', dictionary, Object],
  ["'MH12', String", 'MH12', String],
  ["new String('MH12'), String", new String('MH12'), String],
];
for (const [label, value, C] of cases) {
  console.log(label.padEnd(28), String(value instanceof C).padEnd(6), walkChain(value, C));
}

// The answer follows the chain at the moment you ask, not how the object was made.
Object.setPrototypeOf(bus, null);
console.log('bus after setPrototypeOf(null):', bus instanceof Bus);

// The right side must be callable; a function without a prototype object is refused.
const arrow = () => {};
try {
  ({}) instanceof arrow;
} catch (err) {
  console.log('instanceof an arrow function:', err.name);
}

Output

bus, Bus                     true   true
bus, Vehicle                 true   true
bus, Object                  true   true
lookalike, Bus               true   true
dictionary, Object           false  false
'MH12', String               false  false
new String('MH12'), String   true   true
bus after setPrototypeOf(null): false
instanceof an arrow function: TypeError

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node instanceof-walk.mjs

  • It asks about the prototype chain, not about the constructor. lookalike was made with Object.create(Bus.prototype) and never passed through the Bus constructor, yet it is instanceof Bus. The bus whose prototype was removed is not, although new Bus() made it.
  • The left side must be an object. A primitive such as 'MH12' has no prototype chain of its own, so it is never instanceof String, while new String('MH12'), an object that wraps the text, is.
  • Objects without a prototype belong to nothing. Object.create(null) is not even instanceof Object.
  • The right side must be a function with a prototype object, or have a Symbol.hasInstance method, which comes later in this lesson. An arrow function has no prototype, so ({}) instanceof arrow throws a TypeError. A bound function is looked through: instanceof uses the function it was bound from.

Realms: why instanceof fails across iframes

The standard calls one complete set of built-ins, with its global object and the code that runs with it, a realm. Each realm has its own Object, Array, Error and Map, and their own prototype objects:

An array made in realm B links to realm B prototypes, so instanceof Array in realm A is false while Array.isArray is true.The prototype chain of arrRealm A asks: arr instanceof ArrayIs realm A's Array.prototype on that chain?No: falseRealm A asks: Array.isArray(arr)Is arr itself an array object?Yes: truearr, made in realm B(an iframe, a worker, a vm context)realm B's Array.prototyperealm B's Object.prototypenull: the chain ends[[Prototype]][[Prototype]]

An array from another realm, checked two ways

Text description of the diagram

At the top, the prototype chain of an array called arr, made in realm B, such as an iframe, a worker or a vm context. The chain runs from arr to realm B's Array.prototype, then to realm B's Object.prototype, and ends at null.

Below it, two questions asked by code in realm A. The first, arr instanceof Array, asks whether realm A's Array.prototype appears on that chain. It never does, because the chain holds realm B's prototypes, so the answer is false.

The second, Array.isArray(arr), asks whether arr itself is an array object, whichever realm made it. It is, so the answer is true.

In a browser, every window, iframe and worker is a realm. An iframe on the same origin can hand objects to the page that contains it, and those objects keep their own realm’s prototypes. Node.js creates realms with its node:vm module, which this example uses to make values in a second realm. Node.js documents that node:vm is not a security mechanism; here it only produces values from another realm:

Five values from a vm context, checked from the main realm JavaScript · realm-check.mjs
// A second realm in Node.js: node:vm runs code with its own global object and its own built-ins.
// (node:vm is not a security boundary; it is used here only to make values that come from another realm.)
import vm from 'node:vm';

const other = vm.createContext({});
const make = (source) => vm.runInContext(source, other);

const values = {
  'array [1, 2, 3]': make('[1, 2, 3]'),
  'new TypeError()': make('new TypeError("from the other realm")'),
  'new Date(0)': make('new Date(0)'),
  'new Map()': make('new Map([["Pune", 411001]])'),
  'plain { city }': make('({ city: "Pune" })'),
};
const checks = {
  'instanceof': (v) => v instanceof Array || v instanceof Error || v instanceof Date || v instanceof Map || Object.getPrototypeOf(v) === Object.prototype,
  'isArray/isError': (v) => Array.isArray(v) || Error.isError(v),
  'toString tag': (v) => Object.prototype.toString.call(v),
};
console.log('value'.padEnd(18), 'instanceof  isArray/isError  toString tag');
for (const [name, v] of Object.entries(values)) {
  console.log(name.padEnd(18), String(checks.instanceof(v)).padEnd(11), String(checks['isArray/isError'](v)).padEnd(16), checks['toString tag'](v));
}

// Built-in methods check internal slots, which do not depend on the realm.
console.log('getTime on the other Date:', Date.prototype.getTime.call(values['new Date(0)']));
console.log('has on the other Map:', Map.prototype.has.call(values['new Map()'], 'Pune'));

// Well-known symbols and the Symbol.for registry are shared by every realm, so duck typing still works.
console.log('Symbol.iterator shared:', make('Symbol.iterator') === Symbol.iterator, [...values['array [1, 2, 3]']]);
console.log('Symbol.for shared:', make('Symbol.for("app.order")') === Symbol.for('app.order'));

// A plain object from the other realm has that realm's Object.prototype, whose own prototype is null.
const plain = values['plain { city }'];
console.log('prototype is our Object.prototype:', Object.getPrototypeOf(plain) === Object.prototype);
console.log('prototype of its prototype is null:', Object.getPrototypeOf(Object.getPrototypeOf(plain)) === null);

// structuredClone makes copies in this realm, so instanceof works on them.
console.log('after structuredClone:', structuredClone(values['array [1, 2, 3]']) instanceof Array);

Output

value              instanceof  isArray/isError  toString tag
array [1, 2, 3]    false       true             [object Array]
new TypeError()    false       true             [object Error]
new Date(0)        false       false            [object Date]
new Map()          false       false            [object Map]
plain { city }     false       false            [object Object]
getTime on the other Date: 0
has on the other Map: true
Symbol.iterator shared: true [ 1, 2, 3 ]
Symbol.for shared: true
prototype is our Object.prototype: false
prototype of its prototype is null: true
after structuredClone: true

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node realm-check.mjs

  • instanceof fails for all five, and so does comparing a prototype with this realm’s Object.prototype.
  • Array.isArray and Error.isError say true, because they ask whether the object itself is an array or an error, whichever realm made it.
  • The Object.prototype.toString tag still names the type, because it reads the same internal markers. Any object can fake a tag, though, as the next section shows.
  • Built-in methods check those markers too. Date.prototype.getTime.call(otherDate) works for a date from any realm and throws a TypeError for anything that is not a real date; Map.prototype.has.call does the same for maps. That makes them the realm-proof test for built-ins that have no isX function.
  • Some things are shared. The standard says that well-known symbols such as Symbol.iterator are shared by all realms, and so is the registry behind Symbol.for. That is why duck typing works across realms.
  • Copies belong to the realm that makes them. structuredClone, and postMessage, which uses the same algorithm, create new objects in the receiving realm, so values that arrive that way pass instanceof again.

Ten values, five checks

Each check answers its own question, and a table of ten values shows which:

typeof, Array.isArray, instanceof, Error.isError and the toString tag JavaScript · checks-table.mjs
// Ten values, five checks: which question each check really answers.
const { proxy: revoked, revoke } = Proxy.revocable([], {});
revoke();
const values = {
  '[]': [],
  'new Uint8Array(2)': new Uint8Array(2),
  "{ length: 0 }": { length: 0 },
  'new Proxy([], {})': new Proxy([], {}),
  'new Error()': new Error('boom'),
  'new Proxy(error, {})': new Proxy(new Error('boom'), {}),
  'fake error': { name: 'Error', message: 'boom', stack: '' },
  'tagged as Array': { [Symbol.toStringTag]: 'Array' },
  'null': null,
  'revoked proxy': revoked,
};

const attempt = (fn) => {
  try {
    return String(fn());
  } catch (err) {
    return err.name;
  }
};
console.log('value'.padEnd(22) + 'typeof'.padEnd(8) + 'isArray'.padEnd(10) + 'instanceof Array'.padEnd(18) + 'isError'.padEnd(9) + 'toString tag');
for (const [name, v] of Object.entries(values)) {
  console.log(
    name.padEnd(22) +
      (typeof v).padEnd(8) +
      attempt(() => Array.isArray(v)).padEnd(10) +
      attempt(() => v instanceof Array).padEnd(18) +
      attempt(() => Error.isError(v)).padEnd(9) +
      attempt(() => Object.prototype.toString.call(v)),
  );
}

Output

value                 typeof  isArray   instanceof Array  isError  toString tag
[]                    object  true      true              false    [object Array]
new Uint8Array(2)     object  false     false             false    [object Uint8Array]
{ length: 0 }         object  false     false             false    [object Object]
new Proxy([], {})     object  true      true              false    [object Array]
new Error()           object  false     false             true     [object Error]
new Proxy(error, {})  object  false     false             false    [object Object]
fake error            object  false     false             false    [object Object]
tagged as Array       object  false     false             false    [object Array]
null                  object  false     false             false    [object Null]
revoked proxy         object  TypeError TypeError         false    TypeError

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node checks-table.mjs

  • typeof says 'object' for every value here, null included. It separates primitives and functions from objects, and nothing more.
  • Array.isArray looks through a proxy to its target, so a proxied array counts. A typed array is not an Array, and neither is an array-like object with a length; ArrayBuffer.isView is the realm-proof test for typed arrays. For a revoked proxy, Array.isArray throws a TypeError.
  • instanceof Array agrees with Array.isArray in this table only because every value was made in the same realm.
  • Error.isError says true only for real error objects, the ones that carry the internal marker of an error. It does not look through proxies, and a duck-typed fake with name, message and stack does not fool it.
  • The toString tag comes from internal markers or from Symbol.toStringTag, so tagged as Array reports [object Array]. Use tags in logs, never for decisions.

Duck typing: checking what a value can do

Duck typing asks for the capability you are about to use, and the language itself works this way:

A thenable, two duck-typed checks and a builder named then JavaScript · thenable.mjs
// Duck typing: await and Promise.resolve accept any object with a then method, promise or not.
const courier = {
  then(onFulfilled) {
    console.log('  then() was called by the engine');
    onFulfilled('parcel delivered');
  },
};
console.log(await courier);
console.log(await Promise.resolve(courier));

// The checks behind it: is there a callable then? Is there a callable Symbol.iterator?
const isThenable = (v) => v !== null && (typeof v === 'object' || typeof v === 'function') && typeof v.then === 'function';
const isIterable = (v) => v != null && typeof v[Symbol.iterator] === 'function';
console.log(isThenable(courier), isThenable(Promise.resolve(1)), isThenable({ then: 'after lunch' }));
console.log(isIterable('Pune'), isIterable(new Set([1])), isIterable({ length: 3 }), isIterable(null));

// The catch: an object whose then means something else is treated as a promise too.
class TrainRoute {
  constructor(stops) {
    this.stops = stops;
  }

  then(nextStop) {
    return new TrainRoute([...this.stops, nextStop]); // a builder method that happens to be called then
  }
}
const route = new TrainRoute(['Pune']).then('Lonavala');
console.log(route.stops);
const settled = await Promise.race([
  Promise.resolve(route).then((value) => `resolved with ${typeof value}`),
  new Promise((resolve) => setTimeout(() => resolve('still pending after 50 ms'), 50)),
]);
console.log(settled);

Output

  then() was called by the engine
parcel delivered
  then() was called by the engine
parcel delivered
true true false
true true false false
[ 'Pune', 'Lonavala' ]
still pending after 50 ms

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node thenable.mjs

  • await and Promise.resolve accept thenables. When a promise is resolved with an object, the engine reads its then property, and if it is a function, calls it with two functions that settle the promise. courier is no promise, but awaiting it works.
  • for…of, spread and Array.from accept any iterable: a value whose Symbol.iterator property is a function.
  • Check the capability exactly. typeof value.then === 'function' is the test the engine applies; 'then' in value would also accept { then: 'after lunch' }, which await treats as an ordinary value.
  • A method that happens to be called then is a trap. TrainRoute uses then as a builder method, so awaiting a route makes the engine call it with resolve and reject functions it never calls, and the promise stays pending forever: the race shows it still waiting after 50 milliseconds.

Duck typing works across realms and across copies of a library, and it lets a small object stand in for a class in a test. What it cannot do is tell two unrelated classes with the same method names apart; when that matters, use a brand check, as the last sections show.

Symbol.hasInstance: letting a class answer instanceof

instanceof first looks for a Symbol.hasInstance method on its right side and, when there is one, returns that method’s answer:

A PIN-code check through instanceof, and a check that subclasses inherit JavaScript · has-instance.mjs
// Symbol.hasInstance lets a class answer instanceof itself, even for primitives.
class PinCode {
  static [Symbol.hasInstance](value) {
    return typeof value === 'string' && /^[1-9][0-9]{5}$/.test(value);
  }
}
console.log('411001' instanceof PinCode, '041100' instanceof PinCode, 411001 instanceof PinCode);

// Every function inherits the default from Function.prototype, and that default cannot be overwritten by assignment.
const standard = Object.getOwnPropertyDescriptor(Function.prototype, Symbol.hasInstance);
console.log('default writable:', standard.writable, '| configurable:', standard.configurable);

// Subclasses inherit a custom check, so it can say yes for the wrong class.
class Vehicle {
  static [Symbol.hasInstance](value) {
    return typeof value?.wheels === 'number';
  }
}
class Bus extends Vehicle {}
const autoRickshaw = { wheels: 3 };
console.log('autoRickshaw instanceof Bus:', autoRickshaw instanceof Bus);

Output

true false false
default writable: false | configurable: false
autoRickshaw instanceof Bus: true

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node has-instance.mjs

Every function inherits the default method from Function.prototype, and it performs the prototype walk of the first section. The standard makes that default non-writable and non-configurable, so no code can replace it for every function at once, but a class can define its own static method, as PinCode and Vehicle do.

  • Primitives can pass. '411001' instanceof PinCode is true, because the custom method runs before anything else looks at the left side.
  • Subclasses inherit the method. Bus gets Vehicle’s check, so any object with a number of wheels counts as a Bus. A method that should only answer for its own class must test this === Vehicle first.
  • Use it rarely. Readers expect instanceof to mean “made by this class”. A function named isPinCode(value) says what it checks and surprises nobody; keep Symbol.hasInstance for a brand check that existing instanceof code must keep using.

Checks that survive two copies of a library

The cross-realm failure happens without any iframe when a program loads two copies of one package, for example two versions installed side by side. Each copy evaluates its own class:

A Money object checked by the other copy of its library JavaScript · two-copies.mjs
// Two copies of one library, as when a project ends up with two versions of a package: each copy has its own class.
function loadMoneyLibrary() {
  const BRAND = Symbol.for('example-money-library.Money'); // the same symbol in every copy and every realm

  class Money {
    #paise;

    constructor(paise) {
      this.#paise = paise;
      this[BRAND] = true;
    }

    get paise() {
      return this.#paise;
    }

    static isMoneyOfThisCopy(value) {
      return typeof value === 'object' && value !== null && #paise in value;
    }

    static isMoney(value) {
      return typeof value === 'object' && value !== null && value[BRAND] === true;
    }
  }
  return { Money };
}

const copyA = loadMoneyLibrary();
const copyB = loadMoneyLibrary();
const price = new copyA.Money(4_999);

const checks = {
  'instanceof copyB.Money': price instanceof copyB.Money,
  '#paise in, copy B': copyB.Money.isMoneyOfThisCopy(price),
  'Symbol.for brand, copy B': copyB.Money.isMoney(price),
  'duck typing: paise is a number': typeof price.paise === 'number',
};
for (const [check, answer] of Object.entries(checks)) console.log(check.padEnd(31), answer);

Output

instanceof copyB.Money          false
#paise in, copy B               false
Symbol.for brand, copy B        true
duck typing: paise is a number  true

Recorded with Node.js 24.21.0 on macOS 26 arm64. To run it yourself: mise exec node@24.21.0 -- node two-copies.mjs

The Node.js documentation calls one form of this the dual package hazard, and states that an instanceof comparison of instances created by the two versions returns false. A private-field brand check fails too, because each evaluation of a class body creates new private names. A key from Symbol.for is the same in every copy and every realm, so a brand stored under it is recognised by both copies. It can be faked by any code that knows the key, so it means “made by some copy of this library”, while #paise in value stays the strict check inside one copy. For errors, libraries commonly combine Error.isError(err) with a check of err.name or of a code property they document.

Which check answers which question:

  • Is it an array? Array.isArray(value), in every realm and every copy.
  • Is it an error? Error.isError(value), in every realm and every copy.
  • Is it a typed array or a DataView? ArrayBuffer.isView(value), in every realm and every copy.
  • Is it a date, a map or a set? Call one of its methods, such as Date.prototype.getTime.call(value), and catch the TypeError; this works in every realm and every copy.
  • Can I iterate it, or await it? typeof value[Symbol.iterator] === 'function' and typeof value.then === 'function', in every realm and every copy.
  • Was it made by my class? #field in value, but only for objects of this copy of the class, in this realm.
  • Was it made by my library? A brand under a Symbol.for key works across realms and copies, but code that knows the key can fake it.
JavaScript & TypeScript Online Compiler Write a class with a static Symbol.hasInstance method and test it with primitives, plain objects and a subclass.

Exercise: checks for values from any realm

Exercise · Medium · JavaScript

Type checks that work for values from any realm

A settings library merges the options a page passes in, and some pages pass them from an iframe, where every object has that frame's own prototypes. Write two checks that give the same answer for values from any realm:

  • isPlainObject(value) is true for objects made by an object literal, new Object(), Object.create(null) or Object.create(Object.prototype), and false for everything else: arrays, functions, dates, maps, class instances, null and primitives. In another realm, a plain object's prototype is that realm's Object.prototype, not yours, but it still has no prototype of its own.
  • isIterable(value) is true when for…of would accept value: strings, arrays, maps, sets, generators and any object with a callable Symbol.iterator, own or inherited. It is false for null, undefined, numbers, plain objects and objects whose Symbol.iterator is not a function. It never throws.
isPlainObject({ city: 'Pune' });        // true
isPlainObject(Object.create(null));     // true
isPlainObject([1, 2]);                  // false
isIterable('Pune');                     // true: strings are iterable
isIterable({ length: 2 });              // false: a length is not enough

The sample tests stand in for another realm with objects whose prototypes are not this realm's. The starter code uses instanceof and constructor: run the tests to see which values it gets wrong.

Starter code · checks.mjs

/** True for objects made by {}, new Object(), Object.create(null) or Object.create(Object.prototype), in any realm. */
export function isPlainObject(value) {
  // Replace these lines with your code: instanceof and constructor depend on this realm.
  return value instanceof Object && value.constructor === Object;
}

/** True when for…of accepts value: it has a callable Symbol.iterator. */
export function isIterable(value) {
  return value instanceof Array || value instanceof Map || value instanceof Set;
}
The sample tests · checks.test.mjs
import { test, assert } from 'mysmartcopilot:test';
import { isPlainObject, isIterable } from './checks.mjs';

// Stand-ins for values from another realm: their prototypes are not this realm's.
const otherObjectPrototype = Object.create(null);
const otherArrayPrototype = Object.create(otherObjectPrototype);
otherArrayPrototype[Symbol.iterator] = Array.prototype[Symbol.iterator];
const otherRealmObject = Object.create(otherObjectPrototype);
otherRealmObject.city = 'Pune';
const otherRealmArray = Object.setPrototypeOf(['Pune', 'Goa'], otherArrayPrototype);

test('plain objects made in this realm', () => {
  assert.equal(isPlainObject({}), true);
  assert.equal(isPlainObject({ city: 'Pune', pin: 411001 }), true);
  assert.equal(isPlainObject(new Object()), true);
  assert.equal(isPlainObject(Object.create(Object.prototype)), true);
});

test('objects with no prototype count as plain', () => {
  const dictionary = Object.create(null);
  dictionary.constructor = 'not a function';
  assert.equal(isPlainObject(Object.create(null)), true);
  assert.equal(isPlainObject(dictionary), true);
});

test('a plain object from another realm counts as plain', () => {
  assert.equal(isPlainObject(otherRealmObject), true);
});

test('arrays, functions, built-ins and class instances are not plain', () => {
  class Settings {}
  assert.equal(isPlainObject([]), false);
  assert.equal(isPlainObject(otherRealmArray), false);
  assert.equal(isPlainObject(() => {}), false);
  assert.equal(isPlainObject(new Date(0)), false);
  assert.equal(isPlainObject(new Map()), false);
  assert.equal(isPlainObject(/pune/), false);
  assert.equal(isPlainObject(new Settings()), false);
  assert.equal(isPlainObject(Object.create({ inherited: true })), false);
});

test('null and primitives are not plain objects', () => {
  for (const value of [null, undefined, 0, 'Pune', true, 10n, Symbol('s')]) {
    assert.equal(isPlainObject(value), false, `isPlainObject(${String(value)})`);
  }
});

test('strings, collections and generators are iterable', () => {
  function* stops() {
    yield 'Pune';
  }
  assert.equal(isIterable('Pune'), true);
  assert.equal(isIterable(''), true);
  assert.equal(isIterable([1, 2]), true);
  assert.equal(isIterable(new Map()), true);
  assert.equal(isIterable(new Set()), true);
  assert.equal(isIterable(new Uint8Array(2)), true);
  assert.equal(isIterable(stops()), true);
  assert.equal(isIterable({ [Symbol.iterator]: () => [][Symbol.iterator]() }), true);
});

test('an array from another realm is iterable', () => {
  assert.equal(isIterable(otherRealmArray), true);
  assert.deepEqual([...otherRealmArray], ['Pune', 'Goa']);
});

test('values for…of refuses are not iterable, and nothing throws', () => {
  assert.equal(isIterable(null), false);
  assert.equal(isIterable(undefined), false);
  assert.equal(isIterable(42), false);
  assert.equal(isIterable({}), false);
  assert.equal(isIterable({ length: 2, 0: 'a', 1: 'b' }), false);
  assert.equal(isIterable({ [Symbol.iterator]: 42 }), false);
  assert.equal(isIterable(Object.create(null)), false);
});
A hint

For isPlainObject, rule out null and everything whose typeof is not 'object', then look at Object.getPrototypeOf(value): it is either null, or an object whose own prototype is null. That test does not mention this realm's Object.prototype, so it works for any realm. For isIterable, a value is iterable when it is not null or undefined and typeof value[Symbol.iterator] is 'function'; Symbol.iterator is the same symbol in every realm.

The sample tests run on this device, in your browser (QuickJS): nothing is sent to mysmartcopilot.com. The first run downloads JavaScript (about 0.6 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

Self-check

Check yourself

6 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 6 What does value instanceof Bus check when Bus is an ordinary class?

    Choose one answer.

    Show the answer to question 1

    Answer: Whether Bus.prototype appears anywhere on the prototype chain of value

    instanceof walks the prototype chain of the left side and looks for the right side's prototype property. An object made with Object.create(Bus.prototype) passes without ever running the constructor, and an object whose prototype was changed fails even if the constructor made it.

  2. Question 2 of 6 arr and err were created inside an iframe. Which checks, run in the parent page, give true? Choose every one.

    Choose every answer that is right.

    Show the answer to question 2

    Answer:

    • Error.isError(err)
    • typeof arr[Symbol.iterator] === 'function'
    • Array.isArray(arr)

    Array.isArray and Error.isError look at the object itself, and Symbol.iterator is the same symbol in every realm, so all three work. instanceof and the prototype comparison look for the parent page's Array.prototype and Error.prototype, which never appear on the chain of an object made in the iframe.

  3. Question 3 of 6 In checks-table.mjs, what do Array.isArray and Error.isError say about new Proxy([], {}) and new Proxy(new Error(), {})?

    Choose one answer.

    Show the answer to question 3

    Answer: Array.isArray is true for the proxied array; Error.isError is false for the proxied error

    The standard's IsArray looks through a proxy to its target, so a proxied array is an array. Error.isError checks whether the object itself carries the internal marker of an error, and a proxy does not, so it says false.

  4. Question 4 of 6 TrainRoute has a builder method called then that returns a new route. What happens to await new TrainRoute(['Pune'])?

    Choose one answer.

    Show the answer to question 4

    Answer: await calls its then method like a promise's, nothing ever resolves, and the async function stays suspended

    await treats any object with a callable then as a thenable and calls then with resolve and reject functions. TrainRoute's then never calls them, so the awaited value never settles. Avoid naming a method then unless the object really behaves like a promise.

  5. Question 5 of 6 What does the last line print?

    Read the code, then choose one answer.

    class Vehicle {
      static [Symbol.hasInstance](value) { return typeof value?.wheels === 'number'; }
    }
    class Bus extends Vehicle {}
    console.log({ wheels: 3 } instanceof Bus);
    Show the answer to question 5

    Answer: true

    instanceof calls the static Symbol.hasInstance method it finds on Bus, and Bus inherits Vehicle's. That method accepts anything with a numeric wheels property, so an object literal with three wheels counts as a Bus.

  6. Question 6 of 6 An app loads two copies of a money library. Which check recognises a Money made by the other copy?

    Choose one answer.

    Show the answer to question 6

    Answer: A property keyed by Symbol.for('example-money-library.Money'), set by the constructor

    Each copy evaluates its own class, with its own prototype object and its own private names, so instanceof, constructor comparisons and #paise in all fail across copies. Symbol.for returns the same symbol for the same key in every copy and every realm, so a brand stored under it is recognised by both.

Interview questions

Fresher · Why does Array.isArray exist when instanceof Array works?

Because instanceof Array answers a different question. It asks whether this realm’s Array.prototype is on the value’s prototype chain, and that goes wrong in both directions. An array made in an iframe, a worker or a Node.js vm context has its own realm’s Array.prototype, so instanceof Array says false for a real array; and an object made with Object.create(Array.prototype) says true although it is not an array. Array.isArray asks whether the object itself is an array, whichever realm made it, and it also looks through proxies of arrays. It says false for array-like objects such as { length: 2 } and for typed arrays. Use it for every “is this an array” check; Error.isError, added in ECMAScript 2026, does the same for errors.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A charting library validates its options with value instanceof Array and value instanceof Date. A customer embeds the chart in a same-origin iframe and passes options that the parent page created, and valid options are rejected. Explain why, and rewrite the validation so that it works for values from any realm.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Senior to staff

    After a dependency upgrade, an app's error handler stops recognising errors from an HTTP client: err instanceof HttpError is false, although the stack shows the client threw an HttpError. The lockfile shows two versions of the client installed. Explain the mechanism, the immediate fix, and how the library should make its errors recognisable.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • value instanceof C asks whether C.prototype is on value’s prototype chain right now; it says nothing about which constructor made the value.
  • Each window, iframe, worker and vm context is a realm with its own built-ins, so instanceof fails for values made in another realm.
  • Array.isArray, Error.isError, ArrayBuffer.isView and calls of built-in methods check the object itself and work in every realm; typeof only separates objects from primitives and functions.
  • Well-known symbols and Symbol.for keys are shared by all realms, which is why duck typing on Symbol.iterator or then works everywhere; never name a method then unless it behaves like a promise’s.
  • Symbol.hasInstance lets a class answer instanceof, primitives included, and subclasses inherit it: prefer a named isX function.
  • Two copies of a library have two classes: instanceof and #field in fail across them, and a Symbol.for brand does not.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.