JavaScript has two equality operators. === (strict) checks that the type and the value match, with no conversion. == (loose) converts types first, which leads to famously surprising results:
5 === "5" // false (number vs string)
5 == "5" // true (loose converts, then compares)
null == undefined // true (a special case)
Default to ===. Reach for == only when you specifically want that coercion. The reason is simple: == has a long list of conversion rules that most developers do not memorise, and every implicit conversion is a place where your code can break when data changes.
The strict operator is not just a linter preference — it is a way of making your intentions visible. When another developer sees ===, they know you meant exact equality. When they see ==, they have to stop and wonder whether the conversion was intentional or accidental.
flowchart TD Q["Comparing two values"] --> S["Use === (strict)"] Q --> L["Avoid == (loose)"] S --> R["No type coercion"] style S fill:#e0900b,color:#fff
What == actually does
Loose equality follows the Abstract Equality Comparison Algorithm. You do not need to memorise it, but knowing the shape prevents nasty surprises. The short version is:
- If the types match,
==behaves like===. - If the types differ, both sides are converted to numbers (with a few exceptions) and then compared.
nullandundefinedare equal to each other and nothing else.
That conversion to numbers is where the traps live:
"" == 0 // true (empty string becomes 0)
"0" == false // true (both become 0)
[1, 2] == "1,2" // true (array becomes string)
Because the rules are arbitrary-looking, most teams ban == in their style guides and rely on linters to enforce ===. The tiny amount of extra typing buys a large reduction in bug surface area.
console.log(1 === 1);
console.log(1 === "1");
console.log(null == undefined);
console.log(null === undefined);
console.log("" == 0);
console.log([1,2] == "1,2");
Reference equality: objects and arrays
Two objects (or arrays) are equal only if they are the same object in memory, not merely equivalent contents:
[1, 2] === [1, 2] // false (two different arrays)
const a = [1, 2];
a === a // true (same reference)
This trips up almost every beginner, and for good reason: [1, 2] and [1, 2] look identical. But each array literal creates a new object on the heap. === compares the memory address, not the contents.
The same rule applies to objects, functions, and any non-primitive value. If you need to compare contents, you must do it manually — walk the keys and values — or use a library. There is no built-in deep equality operator in JavaScript.
flowchart LR A1["[1, 2]"] --> M1["memory #101"] A2["[1, 2]"] --> M2["memory #202"] M1 -.->|"==="| R["false"] M2 -.->|"==="| R style M1 fill:#3776ab,color:#fff style M2 fill:#3776ab,color:#fff style R fill:#b45309,color:#fff
The NaN oddity
NaN is the one value that never equals itself. NaN === NaN is false, and NaN == NaN is also false. This is a historical quirk of the IEEE 754 floating-point standard that JavaScript follows.
To test for NaN, use Number.isNaN(value). Do not use value === NaN — it will always be false.
There is one more modern tool: Object.is. It behaves like === except for two cases: Object.is(NaN, NaN) is true, and Object.is(-0, 0) is false (whereas === treats them as equal). You will rarely need Object.is in day-to-day code, but it is useful for library authors who care about edge cases.
console.log(NaN === NaN);
console.log(Number.isNaN(NaN));
console.log(Number.isNaN("hello"));
console.log(Object.is(NaN, NaN));
console.log(Object.is(-0, 0));
flowchart TD
N1["NaN"] --> Q{"=== ?"}
N2["NaN"] --> Q
Q -->|"false"| R["Use Number.isNaN()"]
style Q fill:#b45309,color:#fff
style R fill:#e0900b,color:#fff
What does this print? === refuses to convert; == has a special case for null and undefined.
console.log(5 === "5"); console.log(null == undefined);
5 (number) and "5" (string) have different types under ===.
null and undefined are loosely equal to each other and nothing else.
Two arrays with identical contents are compared with ===. What is the result?
Arrays compare by reference. [1,2] and [1,2] are two separate objects, so === is false even though their contents match.
Write safeEquals(a, b) that returns true if a and b are strictly equal, OR if both are NaN. For all other values use ===. So safeEquals(NaN, NaN) is true, and safeEquals(1, 1) is true, but safeEquals(1, "1") is false.
function safeEquals(a, b) {
// return true if strictly equal or both NaN
}
Use Number.isNaN to detect NaN on both sides.
For everything else, fall back to ===.
function safeEquals(a, b) {
if (Number.isNaN(a) && Number.isNaN(b)) return true;
return a === b;
}
This function is meant to check whether two arrays have the same first element. It fails because it uses loose equality. Fix it by using strict equality.
function sameFirst(a, b) {
return a[0] == b[0];
}
Change == to === so types are compared as well as values.
With ===, 1 and '1' are not equal, and 0 and false are not equal.
function sameFirst(a, b) {
return a[0] === b[0];
}
Write isMissing(value) that returns true only when value is null or undefined. Do NOT use ==. Use strict equality and logical operators instead.
function isMissing(value) {
// return true only for null or undefined, using ===
}
Use === to compare value against null and undefined separately.
Combine the two checks with ||.
function isMissing(value) {
return value === null || value === undefined;
}
Recap
===checks type and value with no conversion; default to it everywhere.==converts types before comparing and produces surprising results; avoid it unless you have a specific reason.- Objects and arrays compare by reference, not by contents. Two identical-looking arrays are different objects.
NaN === NaNisfalse; useNumber.isNaN()to test forNaN.Object.isis like===but treatsNaNas equal to itself and distinguishes-0from0.
Next you'll learn how to declare variables safely and build strings with template literals.