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 }); // falseThe second compares a value to itself.
console.log(NaN === NaN); // falseJavaScript 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.
How === compares primitives and objects
Section titled “How === compares primitives and objects”=== 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'); // trueconsole.log({ id: 1 } === { id: 1 }); // falseconst user = { id: 1 };const alias = user;console.log(user === alias); // trueSo === 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.
Why the course never writes ==
Section titled “Why the course never writes ==”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.
The two edge cases === gets wrong
Section titled “The two edge cases === gets wrong”=== 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); // ?NaN === NaN is false because IEEE 754 , the floating-point standard behind JavaScript’s number type, specifies that NaN equals nothing, not even itself. That makes NaN a reliable “this calculation was invalid” marker, since no equality check can match it back into a valid value. The rule to follow: never compare a value to NaN with ===.
+0 === -0 (and 0 === -0) is true because the spec defines === that way. Signed zero exists and matters in a few niches like graphics and scientific computing, but in everyday web code the distinction stays invisible.
When to use Object.is instead of ===
Section titled “When to use Object.is instead of ===”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.
Number.isNaN over the global isNaN
Section titled “Number.isNaN over the global isNaN”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.
Check your understanding
Section titled “Check your understanding”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 == userBuserA === userBNumber.isNaN(parsedValue)isNaN(parsedValue)Object.is(prevState, nextState)The two correct picks are userA === userB, the default for every primitive and reference comparison, and Number.isNaN(parsedValue), which checks for NaN without coercing.
The three rejected picks:
userA == userBcoerces its operands through rules nobody remembers, and the project’s BiomenoDoubleEqualsrule turns the slip into a build error.isNaN(parsedValue)coerces its argument to a number first, so it returnstruefor'hello'andundefined.Object.is(prevState, nextState)differs from===only in treatingNaNas equal to itself and+0and-0as distinct, which you want for memoization keys and reactivity bailouts. In ordinary code, default to===so the next reader doesn’t assume it’s a typo.
External resources
Section titled “External resources”MDN's side-by-side reference for ==, ===, Object.is, and SameValueZero. The truth tables are exhaustive and worth bookmarking the one time you need to confirm an edge case.
The lint rule that turns == from a discipline problem into a build error. The project's canonical biome.json ships with this enabled.
The full polyfill and the precise wording of how Object.is differs from === — useful when reading a memoizer or a reactivity library and wondering why.