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 === NaNisfalse(NaN never equals anything), and+0 === -0istrue. - 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:
- Same type? Then behave exactly like
===. So'a' == 'a'and{} == {}involve no coercion at all. - null and undefined are equal to each other — and to nothing else.
null == undefinedistrue;null == 0isfalse. - Number vs string: convert the string with
ToNumber, then compare.1 == '1'becomes1 == 1. - Boolean involved? Convert the boolean with
ToNumber(true→ 1,false→ 0), then start over.'1' == truebecomes'1' == 1becomes1 == 1. - BigInt vs number/string: compare mathematical values (
1n == 1istrue), converting strings withStringToBigInt. - Object vs primitive: convert the object with
ToPrimitive— which triesvalueOf(), thentoString()— 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 == 0istruefor an empty field, because'' == 0. With===the type mismatch surfaces immediately. - Validation shortcuts.
if (value == false)accepts0,'', and[]— three values with completely different meanings. - JSON and API data. An API that changes
"42"to42keeps 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 areNaNand-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 isvalue == nullfor "null or undefined". - Turn on
eqeqeqand stop thinking about it — the operator you save three keystrokes with will eventually cost you an afternoon of debugging.