Core Concepts8 min read · Updated 2026-03-10

== vs === in JavaScript: Equality, Coercion, and When Each Actually Applies

Triple equals is the safe default — but understanding why double equals works the way it does reveals something important about JavaScript's type system. Here's the full picture.

💡 Practice these concepts interactively with AI feedback

Start Practicing →

Every JavaScript developer knows "use === not ==". But knowing the rule without understanding why leads to surprises — especially around null, undefined, and object comparisons where the behavior isn't what you'd expect even with ===.

The Core Difference

=== (strict equality): Compares type AND value. If the types differ, it returns false immediately — no conversion, no coercion.

== (loose equality): If the types differ, JavaScript tries to coerce one or both values to the same type before comparing.

1 === 1       // true  — same type, same value
1 === '1'     // false — different types, stops here
1 === true    // false — different types

1 == 1 // true — same type, same value 1 == '1' // true — '1' is coerced to 1 1 == true // true — true is coerced to 1

How === Works

Strict equality follows one rule: same type AND same value. No exceptions, no conversions.

The only gotcha: NaN === NaN is false. NaN is the one value in JavaScript that is not equal to itself. This is defined by the IEEE 754 floating-point standard that JavaScript inherits.

1 === 1           // true
'hello' === 'hello'  // true
true === true     // true
null === null     // true
undefined === undefined  // true

null === undefined // false — different types NaN === NaN // false — NaN is never equal to itself {} === {} // false — different object references [] === [] // false — different array references

The last two are crucial: === on objects and arrays compares references, not contents. Two different objects with identical properties are not strictly equal.

const a = { x: 1 }
const b = { x: 1 }
const c = a

a === b // false — different objects in memory a === c // true — same reference

How == Coercion Works

This is where most developers' knowledge gets fuzzy. The Abstract Equality Comparison algorithm in the spec defines exactly what happens when types differ:

Rule 1: null == undefined is true (and nothing else)

null == undefined   // true — special case, hardcoded in the spec null == 0           // false null == ''          // false null == false       // false undefined == 0      // false undefined == false  // false

This is the one genuinely useful == behavior. Checking value == null catches both null and undefined in one expression. Many linting configs allow this specific pattern.

Rule 2: number vs string — string becomes a number

1 == '1'     // true  — '1' → 1 1 == '01'    // true  — '01' → 1 0 == ''      // true  — '' → 0 0 == '0'     // true  — '0' → 0 0 == ' '     // true  — ' ' → 0 (whitespace-only string → 0)

Rule 3: boolean — boolean becomes a number first

true == 1     // true  — true → 1, then 1 == 1 false == 0    // true  — false → 0, then 0 == 0 true == '1'   // true  — true → 1, then '1' → 1 false == ''   // true  — false → 0, then '' → 0 true == 'true' // false — true → 1, then 'true' → NaN, 1 != NaN

The boolean-to-number conversion catches developers who expect if (value == true) to check for truthiness. It doesn't — it converts true to 1 and then compares numerically.

Rule 4: object vs primitive — object is converted via valueOf/toString

[] == 0       // true  — [] → '' → 0 [] == ''      // true  — [] → '' [] == false   // true  — [] → '' → 0, false → 0 [1] == 1      // true  — [1] → '1' → 1 [[]] == 0     // true  — [[]] → [] → '' → 0

These are the results that make == look broken. They're technically correct per the spec — and exactly why you shouldn't use == with non-primitive values.

The Comparison Table Everyone Needs

// Surprising == results:
'' == '0'         // false
0 == ''           // true
0 == '0'          // true
false == 'false'  // false
false == '0'      // true
false == undefined  // false
false == null     // false
null == undefined   // true
'\t\r\n' == 0    // true

This is not a hypothetical edge case list — every one of these has caused a production bug somewhere.

Object.is() — The Third Equality

ES2015 added Object.is() which is like === but fixes the two edge cases:

// === vs Object.is()
NaN === NaN      // false
Object.is(NaN, NaN)  // true ← fixed

0 === -0 // true Object.is(0, -0) // false ← fixed

// Everything else: same as === Object.is(1, 1) // true Object.is(null, null) // true Object.is({}, {}) // false

React uses Object.is() internally for state comparison — useState and useMemo use it to decide if a re-render or recomputation is needed. The NaN === NaN fix matters there: if state is NaN, React correctly detects that setting state to NaN again is no change.

The One Legitimate Use of ==

// Checking for null OR undefined in one expression:
if (value == null) {
  // runs when value is null or undefined — nothing else
}

// Equivalent to: if (value === null || value === undefined) { // same result, more verbose }

This is the only == pattern worth keeping. It's common enough that ESLint's eqeqeq rule has an explicit null exception option: "eqeqeq": ["error", "always", { "null": "ignore" }].

Interview Questions This Generates

"What does [] == ![] evaluate to?"

[] == ![]
// Step 1: ![] — [] is truthy, so ![] is false
// Step 2: [] == false
// Step 3: false → 0 (boolean to number)
// Step 4: [] → '' → 0 (object to primitive to number)
// Step 5: 0 == 0 → true
[] == ![]  // true

Yes, an empty array equals the negation of an empty array. This is the spec working as intended — and precisely why == with objects is never correct.

"Why is NaN !== NaN?"

NaN means "Not a Number" — the result of an invalid numeric operation. Since different invalid operations produce NaN (Infinity - Infinity, 0/0, parseInt('x')), two NaN values don't necessarily come from the same operation, so they can't be considered equal. Use Number.isNaN() to check for NaN — not === NaN and not the older global isNaN() which coerces before checking.

const result = parseInt('invalid')
result === NaN        // false — always false for NaN
Number.isNaN(result)  // true ← correct check
isNaN('hello')        // true — coerces 'hello' to NaN first, unreliable
Number.isNaN('hello') // false — 'hello' is not NaN, it's a string

The Rule

Use === everywhere. The only exception worth considering is == null to check for both null and undefined simultaneously. For everything else, === gives you predictable, type-safe comparisons with no surprises. When you need to handle NaN or distinguish -0 from 0, use Object.is().

📚 Practice These Topics
Var let const
3–5 questions
Type coercion
4–8 questions
== vs ===
3–5 questions

Put This Into Practice

Reading articles is passive. JSPrep Pro makes you actively recall, predict output, and get AI feedback.

Start Free →Browse All Questions

Related Articles

Core Concepts
map() vs forEach() in JavaScript: Which One to Use and Why It Matters
7 min read
Core Concepts
Arrow Functions vs Regular Functions in JavaScript: 6 Key Differences
9 min read
Interview Prep
Promise.all vs allSettled vs race vs any: The Complete Comparison
9 min read