Learn JS Series (#42) - instanceof, isPrototypeOf, and Checking Types Honestly
Learn JS Series (#42) - instanceof, isPrototypeOf, and Checking Types Honestly

What will I learn
- You will learn how
instanceofactually works: it walks the prototype chain; - how
isPrototypeOfasks the same question more directly; - why
typeofandinstanceofanswer different questions, and when to use each; - the reliable ways to check for arrays, null, and specific object kinds;
- the pitfalls of
instanceof(subclasses, cross-realm objects) and safer alternatives.
Requirements
- A working modern computer running macOS, Windows or Ubuntu;
- An installed Node.js (20+) distribution, or just a modern browser console;
- Episodes 1-41 read, especially the prototype chain and classes.
Difficulty
- Intermediate
Curriculum (of the Learn JS Series):
- Learn JS Series (#1) - What Is JavaScript, Why It Runs Everywhere, and How to Run It
- Learn JS Series (#2) - Variables and Bindings
- Learn JS Series (#3) - The Primitive Types: number, string, boolean, null, undefined, symbol, bigint
- Learn JS Series (#4) - Operators and Expressions: Arithmetic, Comparison, Logical, and Short-Circuiting
- Learn JS Series (#5) - Strings: Template Literals, Unicode, and the Methods You Actually Use
- Learn JS Series (#6) - Numbers: IEEE 754, Why 0.1 + 0.2 Is Not 0.3, and How to Cope
- Learn JS Series (#7) - Control Flow: if/else, switch, and the Ternary Expression
- Learn JS Series (#8) - Loops: for, while, for...of, for...in, and When to Use Which
- Learn JS Series (#9) - Functions: Declarations, Parameters, Return Values, and Hoisting
- Learn JS Series (#10) - Scope and the Temporal Dead Zone: How JavaScript Finds Your Variables
- Learn JS Series (#11) - Arrays: The Workhorse Data Structure and Its Core Methods
- Learn JS Series (#12) - Objects: Key-Value Data, Dot vs Bracket Access, and Nesting
- Learn JS Series (#13) - Truthiness, Equality, and Coercion: == vs === Done Properly
- Learn JS Series (#14) - Mini Project: A Command-Line Tip Calculator
- Learn JS Series (#15) - First-Class Functions: Passing, Returning, and Storing Functions
- Learn JS Series (#16) - Arrow Functions vs function: Syntax, this, and When Each Wins
- Learn JS Series (#17) - Closures: The Single Most Important Idea in JavaScript
- Learn JS Series (#18) - Higher-Order Functions: Functions That Take or Return Functions
- Learn JS Series (#19) - Callbacks and the Callback Pattern (Before We Reach Promises)
- Learn JS Series (#20) - Default, Rest, and Spread: Flexible Function Signatures
- Learn JS Series (#21) - Destructuring Parameters: Named Arguments the JS Way
- Learn JS Series (#22) - The this Keyword: Five Rules That Explain Every Case
- Learn JS Series (#23) - call, apply, and bind: Controlling this Explicitly
- Learn JS Series (#24) - Recursion: Base Cases, the Call Stack, and Stack Overflows
- Learn JS Series (#25) - IIFEs and the Module Pattern (the Pre-2015 Way to Get Privacy)
- Learn JS Series (#26) - Currying and Partial Application
- Learn JS Series (#27) - Function Composition: Building Pipelines from Small Functions
- Learn JS Series (#28) - Pure Functions and Side Effects: The Foundation of Predictable Code
- Learn JS Series (#29) - Memoization: Trading Memory for Speed with Closures
- Learn JS Series (#30) - Mini Project: A Small Functional Utility Library
- Learn JS Series (#31) - Object Literals, Shorthand, and Computed Property Names
- Learn JS Series (#32) - Property Descriptors: writable, enumerable, configurable
- Learn JS Series (#33) - Getters and Setters: Computed Properties That Look Like Data
- Learn JS Series (#34) - The Prototype: JavaScript's Actual Inheritance Mechanism
- Learn JS Series (#35) - The Prototype Chain: How Property Lookup Really Works
- Learn JS Series (#36) - Object.create and Pure Prototypal Inheritance
- Learn JS Series (#37) - Constructor Functions and the new Operator, Step by Step
- Learn JS Series (#38) - The class Syntax: Sugar Over Prototypes, and What It Hides
- Learn JS Series (#39) - Class Inheritance: extends, super, and the Prototype Chain Again
- Learn JS Series (#40) - Static Members, Static Blocks, and Class-Level State
- Learn JS Series (#41) - Private Fields (#) and True Encapsulation
- Learn JS Series (#42) - instanceof, isPrototypeOf, and Checking Types Honestly (this post)
Learn JS Series (#42) - instanceof, isPrototypeOf, and Checking Types Honestly
Last episode we locked data DOWN with # private fields, and right at the end I dropped a little teaser: the #field in obj brand check was, in some ways, a more honest way to ask "is this a real instance of my class?" than the operator most people reach for. That operator is instanceof, and today it gets its day in court. Here is the thing about type checking in JavaScript: almost everyone uses instanceof and typeof every day, and almost nobody can tell you what they actually DO under the hood. They treat them as magic incantations -- "this one for objects, that one for primitives" -- and mostly get away with it, right up until the day one of them lies to their face and they have no idea why. Well, you are not going to be that person. You already understand the prototype chain (episodes 34-36), and once you see that instanceof is nothing more than a walk along that chain, every result it gives you becomes obvious in stead of mysterious. Let's have a proper look. ;-)
Solutions to Episode 41 Exercises
Exercise 1 - a locked-down account:
class Account {
#balance = 0;
deposit(n) { if (n < 0) throw new Error("no negatives"); this.#balance += n; return this.#balance; }
get balance() { return this.#balance; }
}
const a = new Account();
a.deposit(100);
console.log(a.balance); // 100
// a.#balance; // SyntaxError - no outside access at all
The insight: #balance has no back door; the getter is the only read path and deposit is the only write path, so the validation in deposit can never be skipped.
Exercise 2 - a password box:
class PasswordBox {
#value = "";
set(value) { this.#value = value; }
matches(guess) { return guess === this.#value; }
}
const box = new PasswordBox();
box.set("hunter2");
console.log(box.matches("hunter2"), box.matches("wrong")); // true false
The insight: the value is never exposed, only a boolean comparison ever leaves the object, so there is genuinely no way to read the stored password from outside.
Exercise 3 - unique ids via private static:
class Item {
static #next = 1;
#id;
constructor() { this.#id = Item.#next++; }
get id() { return this.#id; }
}
console.log(new Item().id, new Item().id, new Item().id); // 1 2 3
The insight: # privacy is enforced by the language, unlike the _underscore convention which anyone could ignore and quietly corrupt.
Now let's ask the question "what kind of thing is this object?", honestly and correctly.
instanceof walks the prototype chain
The instanceof operator answers: "is this object an instance of that class (or constructor)?". But under the hood it is not checking some stored type tag hidden on the object. It does something you now fully understand: it checks whether the constructor's prototype object appears anywhere in the object's prototype chain (episode 35). That is the entire mechanism, top to bottom:
class Animal {}
class Dog extends Animal {}
const rex = new Dog();
console.log(rex instanceof Dog); // true - Dog.prototype is in rex's chain
console.log(rex instanceof Animal); // true - Animal.prototype is too (via extends)
console.log(rex instanceof Object); // true - Object.prototype is at the top
console.log(rex instanceof Array); // false - Array.prototype is NOT in the chain
rex instanceof Animal is true because extends put Animal.prototype on rex's chain (rex -> Dog.prototype -> Animal.prototype -> Object.prototype -> null). So instanceof naturally respects inheritance: a Dog is an Animal and an Object, because all three prototypes sit on its chain. Knowing it is a chain walk, not a type label, explains every single result it will ever give you. If you can point to where a prototype lives in the chain, you can predict the answer before you run the code.
We can even prove the equivalence by hand. Here is instanceof written out as a plain function, walking the chain link by link exactly the way the operator does internally:
function myInstanceof(obj, Ctor) {
let proto = Object.getPrototypeOf(obj);
while (proto !== null) {
if (proto === Ctor.prototype) return true; // found it in the chain
proto = Object.getPrototypeOf(proto); // step one link up
}
return false; // reached the end (null) without a match
}
console.log(myInstanceof(rex, Animal)); // true - same answer as the real operator
console.log(myInstanceof(rex, Array)); // false
That loop IS instanceof, more or less. It grabs the object's prototype, compares it against Ctor.prototype, and keeps climbing until it either finds a match or falls off the top of the chain at null. Once you have seen it spelled out like this, the operator stops being magic. It is a while loop you could have written yourself.
Customizing instanceof with Symbol.hasInstance
Here is a lovely detail that surprises people: instanceof is not entirely hard-wired. When you write x instanceof C, the engine first looks for a method C[Symbol.hasInstance] and, if it exists, calls it to decide the answer. The default lives on Function.prototype and does the chain walk we just saw, but you can override it to make instanceof mean whatever you like:
class Even {
static [Symbol.hasInstance](value) {
return Number.isInteger(value) && value % 2 === 0;
}
}
console.log(4 instanceof Even); // true - 4 is even
console.log(7 instanceof Even); // false
console.log("x" instanceof Even); // false
Notice we never construct an Even, and the operands are plain numbers, not objects. By defining Symbol.hasInstance, we hijacked instanceof into a custom predicate. This is rarely something you should DO in everyday code (surprising your reader is expensive), but it is worth knowing, because it explains WHY instanceof is a little more flexible than a fixed chain-walk, and it is exactly the kind of hook libraries use to make their objects answer instanceof correctly across tricky boundaries.
isPrototypeOf: the same question, directly
instanceof is really a convenience wrapper around a more direct question. proto.isPrototypeOf(obj) asks: "is proto somewhere in obj's prototype chain?", which is exactly what instanceof checks, just phrased in terms of the prototype object rather than the constructor:
class Animal {}
class Dog extends Animal {}
const rex = new Dog();
console.log(Dog.prototype.isPrototypeOf(rex)); // true - same as rex instanceof Dog
console.log(Animal.prototype.isPrototypeOf(rex)); // true
// works for plain Object.create chains too, where there is no constructor:
const base = { kind: "base" };
const derived = Object.create(base);
console.log(base.isPrototypeOf(derived)); // true - base is in derived's chain
isPrototypeOf is especially handy for the Object.create style (episodes 34-36), where there may be no constructor function to feed to instanceof, you just have bare prototype objects linked together. It is the lower-level primitive; instanceof is the friendlier syntax sitting on top of the same idea. If you ever find yourself thinking "I have the prototype object but not a constructor", isPrototypeOf is the tool you want.
typeof versus instanceof: different questions
Beginners often reach for the wrong one, and then wonder why the answer is useless. typeof and instanceof answer different questions, and knowing which is which is half the battle:
typeofreturns a string naming the primitive type (episode 3):"number","string","boolean","function","object","undefined","symbol","bigint". Use it to distinguish primitives, and to check for"function"and"undefined".instanceofchecks whether an object descends from a particular class or prototype. Use it to check what kind of object something is.
console.log(typeof 42); // "number" - a primitive kind
console.log(typeof "hi"); // "string"
console.log(typeof function(){}); // "function"
console.log(typeof undefined); // "undefined"
console.log([] instanceof Array); // true - what kind of object
console.log(new Date() instanceof Date); // true
console.log(typeof []); // "object" - typeof cannot tell arrays from objects
Notice typeof [] is just "object", useless for distinguishing arrays from plain objects; telling them apart is instanceof's (or Array.isArray's) job. And 42 instanceof Number is false, because the primitive 42 is not an object at all -- instanceof only ever makes sense on the right-hand side of a new. So the rule of thumb writes itself: reach for typeof on primitives, instanceof on objects. Use the wrong tool and you get a technically-true-but-worthless answer like "object" for every non-primitive under the sun.
The reliable checks for common cases
A few specific type checks deserve their own dedicated tools, because the general operators actively mislead you here. These are the ones I want burned into your memory, because you will use them constantly:
For arrays, do not use instanceof Array (it has a cross-context flaw we will get to below); use the purpose-built Array.isArray:
console.log(Array.isArray([1, 2, 3])); // true - the reliable array check
console.log(Array.isArray("abc")); // false
console.log(Array.isArray({ length: 3 })); // false - an array-like is not an array
For null, remember typeof null is the famously buggy "object" (episode 3), a wart that has been in the language since 1995 and can never be fixed without breaking the web. So check for null directly, and combine both facts to test for "a real object":
const value = null;
console.log(value === null); // the correct null check
console.log(typeof value === "object" && value !== null); // "is a real non-null object"
For a precise built-in tag, the classic Object.prototype.toString.call(x) reveals the object's internal [[Class]]-style type name, which is the most reliable way to tell built-in kinds apart:
const tag = (x) => Object.prototype.toString.call(x);
console.log(tag([])); // "[object Array]"
console.log(tag(new Date())); // "[object Date]"
console.log(tag(null)); // "[object Null]"
console.log(tag(/x/)); // "[object RegExp]"
console.log(tag(() => {})); // "[object Function]"
This borrowed-method trick (we used call to borrow a method back in episode 23) gives a reliable internal tag when you genuinely need to distinguish a Date from a RegExp from a plain object. Frontends and libraries lean on it precisely because it survives some of the traps that catch instanceof.
We can wrap all of this good sense into one small helper that gives back a single, honest string for any value you throw at it:
function typeOf(value) {
if (value === null) return "null"; // fix typeof null
if (Array.isArray(value)) return "array"; // fix typeof [] === "object"
return typeof value; // trust typeof for everything else
}
console.log(typeOf([])); // "array"
console.log(typeOf(null)); // "null"
console.log(typeOf(42)); // "number"
console.log(typeOf({})); // "object"
console.log(typeOf(() => {})); // "function"
That tiny function patches the two spots where typeof embarrasses itself (null and arrays) and defers to it everywhere it is already correct. I keep something like this in most codebases, because "what is this, really?" comes up more often than you would think.
The pitfalls of instanceof
instanceof is genuinely useful, but it has two real gotchas that bite people, and both are worth understanding rather than just memorizing. First, it returns true for every ancestor. That is usually what you want, but it means instanceof cannot tell you the exact class, only that the object descends from one somewhere. A Dog is instanceof Animal, so an instanceof Animal test cannot distinguish a Dog from a Cat. When you truly need the precise class, compare the constructor or the immediate prototype directly:
class Animal {}
class Dog extends Animal {}
const rex = new Dog();
console.log(rex instanceof Animal); // true - but so is a Cat
console.log(Object.getPrototypeOf(rex) === Dog.prototype); // true - EXACTLY a Dog
console.log(rex.constructor === Dog); // true - exact class check
Second, the famous cross-realm problem. Because instanceof compares against the identity of a specific prototype object, an array created in a different JavaScript context has a different Array.prototype, and so arr instanceof Array can return false even though the thing is genuinely, undeniably an array. A "realm" here means a separate global environment: a browser iframe, a <script> in another window, a Node vm context, or a worker. Each gets its own fresh set of built-ins:
// Conceptually, in a browser:
// const iframeArray = window.frames[0].Array;
// const arr = new iframeArray(1, 2, 3);
// arr instanceof Array -> false! different Array.prototype per realm
// Array.isArray(arr) -> true. isArray is realm-safe by design
console.log(Array.isArray([1, 2, 3])); // true - the check that always works
This cross-realm trap is exactly why Array.isArray exists in the first place: it does not compare prototypes, it inspects the internal kind, so it is right no matter which realm made the array. The general lesson: prefer purpose-built checks (Array.isArray, the #field in obj brand check from episode 41, or structural "duck typing") over instanceof whenever your values might cross a context boundary. Inside a single, self-contained module instanceof is perfectly fine; the moment iframes, workers, or multiple bundles enter the picture, treat it with suspicion.
Where instanceof genuinely shines: custom errors
Lest this all read as a hit-piece on instanceof, let me show you the case where it is precisely the right tool: distinguishing error types in a catch block. When you define custom error classes and throw them, instanceof lets a single catch sort out what went wrong and respond differently to each kind:
class ValidationError extends Error {}
class NetworkError extends Error {}
function handle(err) {
if (err instanceof ValidationError) return `fix your input: ${err.message}`;
if (err instanceof NetworkError) return `retrying: ${err.message}`;
throw err; // something we did not anticipate - do not swallow it
}
console.log(handle(new ValidationError("email missing"))); // "fix your input: email missing"
console.log(handle(new NetworkError("timeout"))); // "retrying: timeout"
Here you WANT the inheritance-aware behaviour: a ValidationError is also an Error, and asking instanceof ValidationError cleanly separates the failure modes so each gets its own recovery path. This is idiomatic, readable, and exactly what instanceof was designed for. (One small caveat carried over from the realm discussion: if errors can cross module or realm boundaries, some codebases add a name check as a belt-and-braces backup. In a single application, the plain instanceof above is spot on.)
A quick look sideways: type checking in Python and Rust
Quite some of you came to this series from the Learn Python Series, so a short comparison sharpens what JavaScript is doing by showing you the neighbours. Python's isinstance(obj, Cls) is the direct cousin of instanceof: it too is inheritance-aware and returns True for any ancestor class. Python also has type(obj) is Cls for an EXACT class check -- the precise mirror of our obj.constructor === Dog line above:
class Animal: pass
class Dog(Animal): pass
rex = Dog()
print(isinstance(rex, Animal)) # True - like instanceof (any ancestor)
print(type(rex) is Dog) # True - exact class, like constructor === Dog
print(type(rex) is Animal) # False - not the exact class
The parallel is almost one-to-one: isinstance is instanceof, type(x) is C is x.constructor === C. Rust, being statically typed, mostly settles these questions at compile time -- the type of a value is known before the program runs, so there is far less need to interrogate it at runtime, and pattern matching (match) handles the "which variant is this?" cases that JavaScript solves with instanceof on error classes. The takeaway is that "what kind of value is this?" is a universal question every language answers, and JavaScript's dynamic, prototype-based answer is just one point on a spectrum from Python's runtime isinstance to Rust's compile-time guarantees.
Duck typing: an alternative mindset
There is a whole different philosophy worth putting on the table: duck typing -- "if it walks like a duck and quacks like a duck, treat it as a duck". Instead of asking "is this an instance of class X?", you ask "does this object have the capabilities I actually need?". You check for the methods or properties you are going to use, and you do not care one bit about the object's lineage:
function process(thing) {
// don't ask "is it a Stream?"; ask "can it do what I need?"
if (typeof thing.read === "function") {
return thing.read();
}
throw new Error("expected something readable");
}
This structural, capability-based checking is often more flexible and more in keeping with JavaScript's dynamic nature than rigid instanceof checks. It does not care whether thing inherited read from a base class, got it from a mixin (next episode), or just happens to have it as an own method -- it only cares that the method is there. Both approaches earn their keep: instanceof when you genuinely need to know the type or lineage (like the custom-error dispatch above), duck typing when you only care about what the object can DO. Choosing well between the two is a real part of writing idiomatic JavaScript, and honestly it is one of those judgement calls that quietly separates comfortable JS developers from struggling ones.
Try it yourself
- Build a class hierarchy
Vehicle -> Car -> SportsCarwithextends, create aSportsCarinstance, and check it withinstanceofagainst all three classes plusObjectandArray. Explain each result in one sentence in terms of the prototype chain. - Write a function
describeType(value)that returns"array","null","object", or the plaintypeofresult, usingArray.isArray, an explicit=== nullcheck, andtypeof. Test it with an array,null, a plain object, a number, and a function. - Show that
instanceof Animalcannot distinguish aDogfrom aCat(both extendAnimal), then useconstructororObject.getPrototypeOfto check for the exact class. Finish by explaining in one sentence whyArray.isArrayis safer thaninstanceof Array.
So what did we actually cover?
instanceofchecks whether a constructor'sprototypeappears anywhere in the object's prototype chain -- it is literally awhileloop climbing that chain, which is why it naturally respects inheritance.instanceofcan be customized withSymbol.hasInstance;isPrototypeOfasks the same chain question directly in terms of the prototype object, handy forObject.createchains that have no constructor.typeofnames primitive types (and"function");instanceofidentifies object kinds. Usetypeofon primitives,instanceofon objects, and do not swap them.- Use dedicated checks for the tricky cases:
Array.isArrayfor arrays,=== nullfor null, andObject.prototype.toString.call(x)for a precise built-in tag -- a smalltypeOfhelper wraps them all up nicely. instanceofcannot give the exact class (every ancestor matches) and can lie across realms; useconstructor/prototype comparison for exactness andArray.isArray/brand checks for robustness. It shines for custom-error dispatch in acatch.- Duck typing (checking for the capabilities you need rather than the lineage) is often the more flexible, idiomatic alternative when you only care what an object can do.
Next episode we look at sharing behaviour without a single inheritance line: a composition technique for blending capabilities into classes and objects, which will lean directly on the capability-first thinking we just met in duck typing.