Two equality operators
===, called strict equality, is equal only if the type and value both match. No conversions and no surprises.
==, called loose equality, coerces the operands when their types differ, usually toward numbers, before comparing.
That coercion is where the party tricks come from, and 1 == "1", [] == false, and null == undefined are all true.
And one special number deserves its own rule. NaN, meaning not a number and produced by failed math like Number("abc"), is the only value not equal to itself.
Check for it with Number.isNaN(x), never with === NaN. The older global isNaN coerces first, so isNaN("abc") is true for the wrong reason.
Coercion questions are interview standards, and the production reason to care is sharper. == bugs pass code review silently and surface later as corrupted data or impossible states.
Knowing the actual rules is what lets you write === with confidence instead of superstition.
Object.is is a third comparison worth knowing about. It behaves like === except that it treats NaN as equal to itself and distinguishes 0 from -0.
Seven comparisons
Read each line against the rules above.
console.log(1 == "1"); console.log(1 === "1"); console.log(null == undefined); console.log(null === undefined); console.log("" == 0); console.log(NaN === NaN); console.log(Number.isNaN(NaN));
Output
true false true false true false true
1 == "1" coerces the string to a number, and 1 === "1" compares a number to a string and stops there.
null == undefined is a special case written directly into the specification, and it is the basis for the x == null idiom below.
"" == 0 is the trap that catches form input. An empty text field compared loosely to zero looks equal, which is how blank inputs become zeros in a database.
NaN === NaN being false is the self-inequality every interviewer eventually asks about, and it follows from the floating-point standard rather than from JavaScript.
Number.isNaN is the reliable check because it does no coercion. It returns true only for the actual NaN value, so Number.isNaN("abc") is false.
It evaluates to true, because both sides coerce to the number 0.
Loose equality converts both sides. [] becomes "" through its toString, and "" becomes 0, while false becomes 0, so the comparison is 0 == 0.
Meanwhile [] in an if statement is truthy, so if ([]) runs its block.
Coercion in == and truthiness are different systems, which is why the rule in the next block exists. An empty array is falsy under == against false and truthy in a condition, and both are correct.
The reason is that truthiness has a short list of falsy values, namely false, 0, -0, "", null, undefined, NaN, and 0n. Every object is truthy, including [] and {}.
== does not consult that list at all. It runs a conversion algorithm, which is why "0" == false is true while if ("0") runs.
[] == ![] being true is the same joke in one expression, and once you apply the two systems separately it stops being mysterious.
The rule to say out loud
Use === always.
The one accepted exception is x == null, a deliberate idiom that is true exactly when x is null or undefined. It is a compact version of the ?? reasoning from lesson 8-2.
Even that one is optional now, since x ?? fallback and x === null || x === undefined say the same thing more explicitly.
Linters encode this rule as eqeqeq, which is on by default in most configurations, and the smart option permits the == null form.
Comparing objects is the other half of the story, and neither operator helps. {} === {} and {} == {} are both false, because both compare references rather than contents, which lesson 10-3 covers properly.
A bonus float gotcha gets asked in the same breath, and it is not coercion at all. Binary floating point cannot represent 0.1 or 0.2 exactly, so their sum is 0.30000000000000004.
Compare floats with a tolerance rather than equality, which is what the next two blocks build.
Why 0.1 + 0.2 === 0.3 is false
console.log(0.1 + 0.2); console.log(0.1 + 0.2 === 0.3);
Output
0.30000000000000004 false
The answer is false.
Like 1/3 in decimal, 0.1 and 0.2 have no exact binary representation, so each one is stored as the nearest available double.
The rounding errors add up, and the sum lands one representable step above 0.3.
This is not a JavaScript flaw. Every language using IEEE 754 doubles behaves identically, including Python, Java, and C, so the same surprise is waiting everywhere.
The standard fix is a tolerance check, comparing Math.abs(a - b) against a small epsilon rather than testing equality.
Money is the case where tolerance is the wrong answer. Store cents as integers, or use a decimal library, since a tolerance still leaves the arithmetic inexact.
approxEqual
The fix for what you just predicted.
function approxEqual(a, b) { return Math.abs(a - b) < 1e-9; } console.log(0.1 + 0.2 === 0.3); console.log(approxEqual(0.1 + 0.2, 0.3)); console.log(approxEqual(0.1 + 0.2, 0.4));
Output
false true false
The difference may be negative, so it is wrapped in Math.abs before comparing.
1e-9 is a chosen tolerance rather than a magic constant, and picking it is a judgment call about the scale of your data.
Number.EPSILON is the smallest meaningful gap near 1.0, which makes it a reasonable default for values in that range and far too small for large ones.
The tolerance is absolute here, which is the limitation to know. Comparing values near a billion needs a relative test, roughly Math.abs(a - b) < 1e-9 * Math.max(Math.abs(a), Math.abs(b)).
Test frameworks ship this for exactly this reason, and toBeCloseTo in Jest is the same idea with the tolerance expressed as decimal places.