In JavaScript, === (strict equality) returns true only when both values have the same type and the same value, with no conversion. == (loose equality) first converts the two values to a common type and then compares them, so "1" == 1 is true but "1" === 1 is false. Use === by default. The one common exception is value == null, which checks for both null and undefined in one go.
There's also a third option, Object.is(), which behaves like === except for two edge cases: NaN and -0. I'll cover all three, plus how arrays, switch, ESLint and TypeScript fit in.
Every result below comes from scripts I ran on Node.js 24.22.1, with the truth table re-checked on Node.js 18.20.8, 22.23.4 and 26.11.1 (identical output on all four), plus Chrome 156 for the browser examples, TypeScript 7.0.2 and ESLint 10.14.0.
In this guide:
- Quick comparison: ==, === and Object.is()
- How === (strict equality) works
- How == (loose equality) works
- Tested truth table: == vs ===
- The strangest == results, explained
- NaN, -0 and Object.is()
- Which comparison do includes(), indexOf() and switch use?
- The real-world bug: form values are strings
- The one good use of ==: checking for null or undefined
- Enforce === with ESLint and TypeScript
- So which should you use?
- Related guides and tools
- Frequently asked questions
Quick comparison: ==, === and Object.is()
| Feature | == loose | === strict | Object.is() |
|---|
| Converts types first? | Yes | No | No |
"1" vs 1 | true | false | false |
null vs undefined | true | false | false |
NaN vs NaN | false | false | true |
0 vs -0 | true | true | false |
| Two different objects with the same content | false | false | false |
| Typical use | x == null only | Almost everything | NaN or -0 checks, state libraries |
MDN's guide to equality comparisons and sameness calls these IsLooselyEqual, IsStrictlyEqual and SameValue, and adds a fourth, SameValueZero, used by includes(), Map and Set.
How === (strict equality) works
Strict equality asks two questions: are the types the same, and are the values the same? If the types differ, the answer is false immediately. No conversion happens, which is why it's predictable.
For primitives (strings, numbers, booleans, null, undefined, BigInt, symbols), "same value" means what you'd expect. For objects, arrays and functions, it means the same object in memory, not the same contents:
const a = { id: 1 };
const b = { id: 1 };
const c = a;
console.log('a == b ', a == b);
console.log('a === b', a === b);
console.log('a === c', a === c);
console.log('JSON ', JSON.stringify(a) === JSON.stringify(b));
a == b false
a === b false
a === c true
JSON true
Neither operator compares contents. JSON.stringify() works for simple data like this, but it depends on key order and drops undefined values. If you need copies that don't share references, my post on deep copying objects in JavaScript covers the safe options.
How == (loose equality) works
Loose equality runs an algorithm the spec calls IsLooselyEqual. In plain English, it goes roughly like this:
- If both values have the same type, compare them like
===. null and undefined are equal to each other and to nothing else.- Number vs string: convert the string to a number.
- Boolean vs anything: convert the boolean to a number (
true becomes 1, false becomes 0) and start again. - Object vs primitive: convert the object to a primitive with
valueOf() or toString() and start again. - BigInt vs number: compare the mathematical values.
- Anything else is
false.
Rule 4 causes the most surprises. true == "true" doesn't compare two "true" things. It turns true into 1, turns "true" into NaN, and returns false.
Tested truth table: == vs ===
I ran these pairs through both operators. Here's the exact output from Node.js 24.22.1:
"1" == 1 true === false
0 == "" true === false
0 == "0" true === false
"" == "0" false === false
false == "0" true === false
false == "" true === false
false == 0 true === false
null == undefined true === false
null == 0 false === false
undefined == 0 false === false
null == false false === false
NaN == NaN false === false
" \t\n" == 0 true === false
"1,2" == [1,2] true === false
true == "true" false === false
true == 1 true === false
[] == false true === false
[] == "" true === false
Two lines show why == is hard to reason about. 0 == "" and 0 == "0" are both true, but "" == "0" is false. Loose equality isn't transitive, so you can't chain your intuition. Meanwhile === said false for every row, because every pair mixes types.
The strangest == results, explained
A few more results from the same test run, each with the reason:
[] == ![] true
null == 0 false | null >= 0 true | null > 0 false
'10' == 10.0 true | '0x10' == 16 true | '1e3' == 1000 true
d == 0 false | d == d.toString() true
1n == 1 true | 1n === 1 false
[] == ![] is true. ![] is false (arrays are truthy), false becomes 0, [] becomes "", which becomes 0.null >= 0 is true but null == 0 is false. Relational operators convert null to 0. Loose equality has its own rule: null only equals undefined.'0x10' == 16 is true. String-to-number conversion understands hex and exponent notation.new Date(0) == 0 is false. Dates convert to their string form by default, so a date equals its toString() but not its timestamp. Compare d.getTime() instead.1n == 1 is true, 1n === 1 is false. BigInt and number are different types.
Loose equality can also run your code. This object has a custom valueOf():
const price = { valueOf() { console.log(' valueOf called'); return 42; } };
console.log('price == 42 ->', price == 42);
console.log('price === 42 ->', price === 42);
valueOf called
price == 42 -> true
price === 42 -> false
== called valueOf() behind the scenes. === didn't.
NaN, -0 and Object.is()
NaN is the only value in JavaScript that isn't equal to itself, under both == and ===. That means x === NaN is always false, even when x is NaN:
const n = Number('abc');
console.log('n === NaN', n === NaN, '| Number.isNaN(n)', Number.isNaN(n), '| Object.is(n, NaN)', Object.is(n, NaN));
console.log('0 === -0', 0 === -0, '| Object.is(0, -0)', Object.is(0, -0));
n === NaN false | Number.isNaN(n) true | Object.is(n, NaN) true
0 === -0 true | Object.is(0, -0) false
Object.is() fixes both edge cases: it treats NaN as equal to itself and tells 0 and -0 apart. React uses the same comparison to decide whether state has changed. For everyday NaN checks, Number.isNaN() reads better.
Which comparison do includes(), indexOf() and switch use?
Built-in methods pick their own comparison, and they don't all agree:
indexOf(NaN) -1 | includes(NaN) true | includes(-0) true
switch('1') string one | switch(1) number one
| Feature | Comparison used | Finds NaN? |
|---|
indexOf(), lastIndexOf() | Strict (===) | No |
includes(), Map, Set | SameValueZero | Yes, and treats -0 as 0 |
switch / case | Strict (===) | No |
Object.is() | SameValue | Yes, and keeps -0 separate |
The switch row matters in real code. A case 1: will never match the string "1", so convert input before you switch on it.
The real-world bug: form values are strings
Most == versus === bugs in practice come from user input. A value read from an input, a URL parameter or localStorage is always a string, even with type="number". I checked in Chrome 156 with <input type="number" value="5">:
document.getElementById('q').value -> "5"
typeof document.getElementById('q').value -> "string"
document.getElementById('q').value === 5 -> false
document.getElementById('q').valueAsNumber === 5 -> true
And in Node.js, with a string standing in for the input value:
input == qty true | input === qty false | Number(input) === qty true
Switching from === to == makes the symptom go away, but the real fix is to convert once, at the edge, with Number() or valueAsNumber, and then compare strictly. My guide to HTML form input types and validation shows how to keep form data clean before it reaches your logic.
The one good use of ==: checking for null or undefined
value == null is true for exactly two values, which makes it a compact "is it missing?" check:
null == null -> true
undefined == null -> true
0 == null -> false
"" == null -> false
false == null -> false
It doesn't catch 0, "" or false, which is usually what you want. Modern code often uses optional chaining (?.) and nullish coalescing (??) instead, because they apply the same null-or-undefined rule. If you're chasing an actual crash, my guide to Cannot read properties of undefined walks through those operators with tested examples.
Enforce === with ESLint and TypeScript
You don't have to rely on memory. The ESLint eqeqeq rule flags every ==, and its null: "ignore" option allows the == null idiom. With this config:
export default [
{
files: ["**/*.js"],
rules: {
eqeqeq: ["error", "always", { null: "ignore" }],
},
},
];
ESLint 10.14.0 flagged line 3 and skipped the null check on line 4:
/workspace/seo-posts-nov02/test/js/lint/cart.js
3:11 error Expected '===' and instead saw '==' eqeqeq
✖ 1 problem (1 error, 0 warnings)
Running eslint --fix left that line unchanged and reported the same error. The rule's docs say only some of its problems are auto-fixable, and a string compared with a number wasn't one of them in my test. That's sensible, because swapping in === here would change the result, so you have to decide whether to convert the value first.
TypeScript catches the same mistake earlier. Comparing a number with a string using === failed to compile with TypeScript 7.0.2:
check.ts(4,5): error TS2367: This comparison appears to be unintentional because the types 'number' and 'string' have no overlap.
Changing the condition to qty === Number(input) compiled with no errors.
So which should you use?
- Use
=== and !== by default. They never convert types, so what you read is what runs. - Convert input explicitly with
Number(), String() or valueAsNumber before comparing. - Use
== null only if your team agrees on it, and allow it in ESLint with null: "ignore". - Use
Number.isNaN() or Object.is() for NaN and -0. - Compare objects by a key, such as
a.id === b.id, not by reference, unless you really mean "the same object".
If you're tidying up older JavaScript, switching to === pairs naturally with replacing var; see var vs let vs const for that side.
Related guides and tools
For more quick browser utilities, including a word counter, password generator and image resizer, see the free tools on mfaysal.com.
Frequently asked questions
What is the difference between == and === in JavaScript?
=== checks that two values have the same type and the same value, with no conversion. == converts the values to a common type first, so "5" == 5 is true while "5" === 5 is false.
Should I always use === in JavaScript?
Use === by default. It is predictable and linters such as ESLint enforce it with the eqeqeq rule. The only common exception is value == null, which checks for both null and undefined.
Why is null == undefined true but null === undefined false?
The loose equality algorithm has a special rule that null and undefined equal each other and nothing else. Strict equality sees two different types, so it returns false.
Why is NaN === NaN false?
NaN is defined as not equal to anything, including itself, under both == and ===. Use Number.isNaN(x) or Object.is(x, NaN) to test for it.
Is === faster than ==?
When the types already match, both do the same work, so any difference is tiny. Pick === for correctness and readability, not speed.
Does === compare objects and arrays by content?
No. Both == and === compare objects by reference, so two separate arrays with the same items are not equal. Compare a unique key, or compare the items yourself.
What does != vs !== mean?
They are the negated versions. != uses loose equality and converts types, while !== uses strict equality. "5" != 5 is false, but "5" !== 5 is true.
When should I use Object.is() instead of ===?
Use Object.is() when you need NaN to equal NaN or need to tell 0 and -0 apart. For everything else, === gives the same result and is easier to read.
Last checked: 2 November 2026. Figures, settings and guidance can change, so confirm anything important with the sources linked above.