== vs ===: Understanding JavaScript Equality

Learn the difference between loose and strict equality in JavaScript.

The Simple Rule

Always use === unless you have a specific, documented reason to use ==. That is the whole recommendation. The rest of this guide explains what each operator actually does — because the reason == causes bugs is not that it is "loose", it is that it runs a multi-step coercion algorithm most developers have never read.

How === Works (Strict Equality)

Strict equality is genuinely simple. The spec's IsStrictlyEqual operation says:

  • If the operands have different types, return false. No coercion, ever.
  • For numbers: NaN === NaN is false (NaN never equals anything), and +0 === -0 is true.
  • For objects (including arrays and functions): compare references — two objects are equal only if they are the same object.
1 === '1'            // false — number vs string
null === undefined   // false — different types
NaN === NaN          // false — the NaN exception
[1] === [1]          // false — different array objects

How == Actually Works: The Abstract Equality Algorithm

== (IsLooselyEqual in the spec) follows these steps, in order:

  1. Same type? Then behave exactly like ===. So 'a' == 'a' and {} == {} involve no coercion at all.
  2. null and undefined are equal to each other — and to nothing else. null == undefined is true; null == 0 is false.
  3. Number vs string: convert the string with ToNumber, then compare. 1 == '1' becomes 1 == 1.
  4. Boolean involved? Convert the boolean with ToNumber (true → 1, false → 0), then start over. '1' == true becomes '1' == 1 becomes 1 == 1.
  5. BigInt vs number/string: compare mathematical values (1n == 1 is true), converting strings with StringToBigInt.
  6. Object vs primitive: convert the object with ToPrimitive — which tries valueOf(), then toString() — and start over with the resulting primitive.

Two things follow immediately from these rules. First, booleans are always converted to numbers, which is why 'true' == true is false: it becomes 'true' == 1, then NaN == 1. Second, objects are compared to primitives by serializing themselves, which produces the famous absurdities.

Worked Examples

'' == 0
// Step 3: ToNumber('') is 0  →  0 == 0  →  true

'0' == false
// Step 4: false → 0          →  '0' == 0
// Step 3: ToNumber('0') is 0 →  0 == 0  →  true

[] == false
// Step 4: false → 0          →  [] == 0
// Step 6: [] → '' (toString) →  '' == 0
// Step 3: '' → 0             →  0 == 0  →  true

[] == ![]
// ![] evaluates first: [] is truthy, so ![] is false
// → [] == false → true (chain above)

[null] == ''
// [null].toString() is ''    →  '' == ''  →  true

new String('a') == 'a'
// Step 6: object → 'a'       →  'a' == 'a' →  true (=== says false)

The Real Problem: == Is Not Transitive

0 == ''     // true
0 == '0'    // true
'' == '0'   // false

An equality relation you cannot reason about transitively is an equality relation you cannot refactor safely. That — more than any single weird pair — is why style guides ban it.

Where the Bugs Actually Come From

Real code rarely compares literals. The bugs appear where types are already uncertain:

  • Form inputs and URL params are strings. input.value == 0 is true for an empty field, because '' == 0. With === the type mismatch surfaces immediately.
  • Validation shortcuts. if (value == false) accepts 0, '', and [] — three values with completely different meanings.
  • JSON and API data. An API that changes "42" to 42 keeps working by accident with ==, then breaks somewhere far away.
function isValid(value) {
  if (value == null) return false; // intentional? or a bug for 0 and ''?
  return true;
}

With == the reader cannot tell whether coercion was intended. With === intent is explicit.

The One Legitimate Use of ==

Comparing against null with == matches both null and undefined and nothing else — a deliberate, well-defined idiom:

if (value == null) {
  // value is null OR undefined — nothing else passes
}

This is exactly equivalent to value === null || value === undefined, and it is safe because step 2 of the algorithm involves no coercion. Even so, modern syntax often removes the need:

const name = maybeName ?? 'anonymous'; // nullish coalescing
const city = user?.address?.city;      // optional chaining

A Third Option: Object.is()

Object.is() is strict equality with the two number quirks fixed:

Object.is(NaN, NaN)  // true   (=== says false)
Object.is(0, -0)     // false  (=== says true)
Object.is(1, '1')    // false, like ===

Use it when NaN or signed zero actually matter — polyfills, memoization caches, React's dependency comparisons. Everywhere else, === reads better.

Reference Table

x y x == y x === y
1 '1' true false
null undefined true false
0 false true false
'' false true false
[] false true false
'true' true false false
NaN NaN false false
[1] [1] false false

Enforce It Automatically

Add the eqeqeq rule to ESLint. The smart option allows the one good idiom while banning everything else:

{
  "rules": {
    "eqeqeq": ["error", "smart"]
  }
}

smart permits == null comparisons and same-type literal comparisons, and errors on every coercing case.

Summary

  • === compares type and value, with no conversions; the only surprises are NaN and -0.
  • == runs a recursive coercion algorithm: strings become numbers, booleans become numbers, objects become primitives — and the result is not even transitive.
  • The single defensible == idiom is value == null for "null or undefined".
  • Turn on eqeqeq and stop thinking about it — the operator you save three keystrokes with will eventually cost you an afternoon of debugging.