The difference between == and === in JavaScript is not a style preference — it decides whether your comparison coerces types before comparing. === compares values without conversion, == applies a set of coercion rules before it looks at anything else. Understanding those rules is the difference between fixing a comparison bug in ten minutes and guessing at it for an afternoon.
What is the difference between == and ===?
=== is the strict equality operator: two values are equal only if they are the same type and have the same value. == is the loose one: if the types differ, it converts one of them before comparing.
console.log(1 == 1, 1 === 1); // true true
console.log(1 == "1", 1 === "1"); // true false
console.log(0 == false, 0 === false); // true false
console.log("" == false, "" === false); // true false
console.log(null == undefined, null === undefined); // true false
console.log(NaN == NaN, NaN === NaN); // false false
console.log(0 === -0); // true
console.log({} == {}, {} === {}); // false false
Read that output as a table of surprises. The number 1 and the string "1" are loosely equal but not strictly equal. Zero and false are loosely equal. The empty string and false are loosely equal. null and undefined are loosely equal but not strictly equal. Two different object literals are never equal, loose or strict, because each literal is a distinct object.
What exactly does == coerce?
The algorithm is specific, and it explains almost every loose comparison you will meet.
console.log("5" == 5); // true
console.log("5" === 5); // false
console.log("5" + 5); // "55" ← + concatenates
console.log("5" - 5); // 0 ← - coerces to numbers
console.log(true == 1); // true
console.log(true == "1"); // true
console.log([] == false); // true
console.log([1] == 1); // true
Here is the rule set in plain terms. If both operands have the same type, == behaves exactly like ===. If one is null and the other is undefined, they are equal. Otherwise, the engine converts one operand to the type of the other and compares again.
The last two lines are where it gets strange. false converts to 0. An empty array converts to an empty string, which then converts to 0. An array with one element converts to that element's string form, so [1] becomes "1", which becomes 1. Every step is a documented rule, but the chain is long enough that relying on it is a bad idea.
Why does null == undefined return true?
Because == has one special case for them, and === does not. The two values are genuinely different — one means "deliberately empty", the other means "no value provided" — but the loose operator treats them as interchangeable.
console.log(null == undefined); // true
console.log(null === undefined); // false
console.log(null == 0); // false
console.log(undefined == 0); // false
console.log(null == false); // false
console.log(null > 0); // false
Notice that null is only loosely equal to undefined — not to 0 and not to false. That special case is narrow and deliberate.
If you want to test for "no value" and mean both, write the check explicitly rather than relying on the coercion: value === null || value === undefined, or the shorter value == null if you are comfortable with exactly one loose comparison in your codebase. Many teams ban == outright and use ?? for the same job.
What about NaN and negative zero?
NaN is the only value that is not equal to itself, under either operator. It means "not a number", and it is produced by arithmetic that cannot produce a number — 0 / 0, Math.sqrt(-1), or converting the string "hello" with Number().
console.log(NaN === NaN); // false
console.log(NaN == NaN); // false
console.log(Number.isNaN(NaN)); // true
console.log(0 === -0); // true
console.log(Object.is(0, -0)); // false
Negative zero deserves a moment. It is a real thing in IEEE 754 — it comes out of arithmetic like -1 * 0 — and === considers it equal to zero. It matters rarely, but when it does (a physics calculation, a direction check) you need Object.is.
There is a related detail that shows up when searching arrays. Array.prototype.includes uses a comparison that treats NaN as equal to NaN, while indexOf does not:
console.log([NaN].indexOf(NaN)); // -1
console.log([NaN].includes(NaN)); // true
What is Object.is for?
Object.is(a, b) is a third comparison function, added with the rest of the ES2015 improvements. It is like === except that it distinguishes NaN from everything and treats 0 and -0 as different.
console.log(Object.is(1, 1)); // true
console.log(Object.is(NaN, NaN)); // true
console.log(Object.is(0, -0)); // false
console.log(Object.is("a", "a")); // true
console.log(Object.is({}, {})); // false
Use Object.is when you need the mathematically precise answer and === everywhere else. It is not more correct in general — it is more precise about a narrower set of cases. The one place it is clearly right is a NaN check, where Object.is(x, NaN) says yes and x === NaN always says no.
So which operator should I use?
===, essentially always. It compares what you think it compares. The exceptions are narrow enough to list.
- Use
===for all value comparisons, including strings and numbers. - Use
== nullonly if you deliberately want to match both null andundefined, and only if your style guide allows it. - Use
Number.isNaN(value)rather thanvalue === NaNto test for NaN. - Use
Object.iswhen you need to distinguish0from-0, or to compareNaNvalues. - Remember that
switchuses===internally, so aswitchon2will not match the case"2".
The real danger of == is not that it is wrong. It is that it is right often enough to survive code review. A comparison that converts "5" to a number when you meant to catch a string from an API response will not fail in your tests — it will fail in production, on the one request that returns a string where you expected a number.
The equality rules in one page
===compares type and value.==compares type and value after coercing.null == undefinedistrue;null === undefinedisfalse.NaNis not equal to itself under either operator. UseNumber.isNaN.- Objects and arrays are compared by identity: two literals are never equal, but the same reference always is.
0and-0are===but notObject.is.- Loose comparison with an empty array walks through string and number coercion. Do not rely on it.
Try it yourself: write a function isSameId(a, b) that decides whether two values represent the same identifier, given that one may arrive as a number from a database and the other as a string from a query string. Test it with isSameId(42, "42"), isSameId(42, 42) and isSameId(null, undefined). Then write a second version that refuses to coerce and returns false for anything but an exact match, and decide which one you would rather debug at three in the morning.