Skip to content
Chapter 1Lesson 2

What === actually compares

JavaScript's equality operators, and why strict equality is the one you reach for by default.

Two snippets surprise almost every developer the first time they meet them. The first compares two objects with identical contents.

console.log({ id: 1 } === { id: 1 }); // false

The second compares a value to itself.

console.log(NaN === NaN); // false

JavaScript has three equality operators, plus two edge cases worth knowing. The senior habit is to reach for one by default and recognize the rest on sight. This lesson makes === your default, shows the two cases where its answer can mislead you, and names the rare moments another operator fits better.

=== asks one question: are these the same value? The answer depends on the kind of value you hand it, the same split you saw in the previous lesson on bindings. Primitives are values themselves; objects are references to a value that lives elsewhere.

For primitives, === tests value equality: it compares the value itself. 'ada' === 'ada' is true because the two strings are the same value: a primitive has no identity separate from its content. 42 === 42, true === true, and null === null are all true for the same reason.

For objects, === tests reference equality (also called identity equality): it compares the reference, not the contents it points at. Two object literals with identical contents are two separate allocations, so their references differ and === returns false. The same object reached through two names is === to itself, because both names hold the same reference.

console.log('ada' === 'ada'); // true
console.log({ id: 1 } === { id: 1 }); // false
const user = { id: 1 };
const alias = user;
console.log(user === alias); // true

So === answers “the same value?” for primitives and “the same allocation?” for objects. It never answers “the same shape?”: the language has no operator for that, and we return to it at the end of the lesson.

JavaScript has a second equality operator, ==, and the course never uses it.

Before comparing, == runs a table of coercion rules: strings become numbers, objects become primitives, null equals undefined but not false, and the special cases pile up from there. The table is documented, but nobody recalls it correctly under deadline pressure. The one occasionally defensible use, x == null to match both null and undefined, is unnecessary here; the next chapter covers the ?? operator and explicit nullish checks instead.

You don’t have to police this by hand. Biome’s noDoubleEquals rule, in the biome.json you set up later, flags every == and != at lint time, so a slip becomes a build error instead of a review catch.

=== is the right default everywhere, but two cases will surprise you. Predict the output before you read on.

Predict what this program prints, then press Check.

console.log(NaN === NaN); // ?
console.log(+0 === -0); // ?
console.log(0 === -0); // ?

JavaScript has a third equality form, Object.is(a, b), which despite its name has nothing to do with objects. It matches === everywhere except the two edge cases above.

console.log(Object.is(NaN, NaN)); // true — diverges from ===
console.log(Object.is(+0, -0)); // false — diverges from ===
console.log(Object.is('ada', 'ada')); // true — same as ===
console.log(Object.is({}, {})); // false — same as ===

Two real cases call for it. The first is a custom memoization key where bit-pattern identity matters, such as a memoizer that treats NaN as a valid input distinct from “no value.” The second is React’s reactivity bailout: a state setter uses Object.is to decide whether the value changed, so passing a freshly spread object re-renders even when its contents look identical, because the spread is a new reference. We return to this in the React chapters.

Default to ===, and reach for Object.is only in those two cases. When you do, add a comment saying why, or the next reader will take it for a typo.

Because NaN === NaN is false, you can’t test for NaN with x === NaN. JavaScript gives you two dedicated functions for the job, and only one is safe.

The global isNaN(x) coerces its argument to a number first. So isNaN('hello') is true, not because 'hello' is NaN but because coercing it to a number produces NaN. The function answers a different question than its name suggests.

Number.isNaN(x) skips the coercion: it returns true only when the value you pass is the actual NaN. Run the two side by side to see where they disagree.

Predict what each form returns for the five inputs, then run. The actual results come from inputs.map(isNaN) and inputs.map(Number.isNaN) — your job is to fill in the prediction arrays and see whether they match. Pay attention to where the two forms disagree.

    The same split holds for Number.isFinite(x) versus isFinite(x). Whenever you check a property of a number, reach for the Number.* functions; the unprefixed globals coerce.

    JavaScript has no structural-equality operator

    Section titled “JavaScript has no structural-equality operator”

    One comparison is missing from the language: checking whether two objects have the same shape and the same values. There is no built-in operator or function for it, so { id: 1, name: 'Ada' } and a separate object with identical fields can only be compared by a third-party library or a hand-written recursive walk.

    The course avoids the question instead. Structural equality rarely needs to be written by hand when the data model is shaped well, and three patterns cover the common cases:

    • Compare by primary key. Two invoice rows are the same invoice when a.id === b.id. Database-shaped data carries its identity in the ID column, so equality reduces to comparing two strings.
    • Derive a stable string key. To detect changes for caching or deduplication, build a key from the relevant fields (`${invoiceId}:${status}:${updatedAt}`) and compare strings.
    • Use a discriminated union. When each variant has its own shape, the comparison that matters is on the discriminant field, not the whole object. A later TypeScript chapter covers this.

    At the network and database boundaries, well-shaped code compares by ID by construction, so deep comparison rarely comes up.

    Each option stands alone, so judge them one at a time.

    Which of these expressions should you write in 2026 SaaS code? Select all that apply.

    userA == userB
    userA === userB
    Number.isNaN(parsedValue)
    isNaN(parsedValue)
    Object.is(prevState, nextState)