JavaScript == vs ===: Loose vs Strict Equality Explained
Compare JavaScript's == and === operators: how coercion works, NaN and null quirks, Object.is differences, and when loose equality is actually useful.
JavaScript == vs ===: Loose vs Strict Equality
JavaScript ships two equality operators and they behave very differently. === (strict equality) compares without any type conversion: if the operands are different types, the result is always false. == (loose equality) runs the Abstract Equality Comparison algorithm from the ECMAScript spec, which coerces operands toward a common type before comparing.
How each operator works
Strict equality (===): same type required. Numbers compare by value, strings by contents, booleans by value, and objects by reference. No conversion happens.
Loose equality (==): if the types match, it behaves like ===. If they differ, JavaScript applies rules such as: null == undefined, numbers and strings are compared after the string is coerced to a number, booleans are coerced to numbers, and objects are coerced via ToPrimitive when compared with a primitive.
Comparison table
| Expression | == | === |
|---|---|---|
1 == "1" | true | false |
0 == false | true | false |
0 == "" | true | false |
"" == false | true | false |
null == undefined | true | false |
null == 0 | false | false |
null == false | false | false |
NaN == NaN | false | false |
NaN != NaN | true | true |
[] == false | true | false |
[] == ![] | true | false |
[1] == "1" | true | false |
{} == {} | false | false |
"0" == false | true | false |
The NaN gotcha
NaN is never equal to anything, including itself. Both NaN === NaN and NaN == NaN return false, and both NaN !== NaN and NaN != NaN return true. Use Number.isNaN(x) to test for it reliably. The older global isNaN() coerces its argument first, so isNaN("foo") is true — usually not what you want.
The null and undefined relationship
Under loose equality, null and undefined only equal each other and nothing else. So null == undefined is true, but null === undefined is false because their types differ. Crucially, null == 0 is false — the algorithm short-circuits and does not coerce null to a number.
Object equality is reference identity
Both operators compare objects by reference, not by structure. Two distinct object literals with identical contents are never equal:
const a = { x: 1 };
const b = { x: 1 };
a === b; // false
a == b; // false
a === a; // true
For structural comparison, write a deep-equal helper or use one from a library (Lodash isEqual, Node's assert.deepStrictEqual).
Object.is vs ===
Object.is(a, b) is nearly identical to ===, with two intentional differences:
Object.is(NaN, NaN)istrue, whileNaN === NaNisfalse.Object.is(+0, -0)isfalse, while+0 === -0istrue.
Everything else behaves the same. React uses Object.is semantics for its state and dependency comparisons for exactly these reasons.
Modern advice
- Default to
===and!==. The coercion rules of==are notoriously error-prone and rarely what you actually want. - Use
foo == nullas an intentional idiom to check for "nullish" values — it matches bothnullandundefinedin a single check and is widely recognized. If you prefer,foo === null || foo === undefinedor the nullish coalescing operatorfoo ?? fallbackexpress the same intent explicitly. - Use
Number.isNaN(x)to test forNaN, orObject.is(x, NaN). - Never rely on
==for numeric-string comparisons. Convert explicitly withNumber(str)orparseInt(str, 10).
Enforce it with ESLint
The eqeqeq rule flags every use of == and !=. Enable it in your config:
// .eslintrc.json
{
"rules": {
"eqeqeq": ["error", "always", { "null": "ignore" }]
}
}
The { "null": "ignore" } option keeps the useful foo == null idiom while blocking every other loose comparison. This pairing — strict equality everywhere plus one deliberate exception — is the pragmatic modern default used by most production JavaScript and TypeScript codebases today.