# == vs === in JavaScript: Loose Equality vs Strict Equality vs Object.is() Explained (Comparison Table and Tested Examples)

**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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/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:

```javascript
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](https://mfaysal.com/blog/javascript-deep-copy-object) covers the safe options.

## How == (loose equality) works

Loose equality runs an algorithm the spec calls [IsLooselyEqual](https://tc39.es/ecma262/#sec-islooselyequal). In plain English, it goes roughly like this:

1. If both values have the same type, compare them like `===`.
2. `null` and `undefined` are equal to each other and to nothing else.
3. Number vs string: convert the string to a number.
4. Boolean vs anything: convert the boolean to a number (`true` becomes 1, `false` becomes 0) and start again.
5. Object vs primitive: convert the object to a primitive with `valueOf()` or `toString()` and start again.
6. BigInt vs number: compare the mathematical values.
7. 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()`:

```javascript
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`:

```javascript
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()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/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](https://mfaysal.com/blog/html-form-input-types-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](https://mfaysal.com/blog/javascript-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](https://eslint.org/docs/latest/rules/eqeqeq) flags every `==`, and its `null: "ignore"` option allows the `== null` idiom. With this config:

```javascript
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](https://mfaysal.com/blog/javascript-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](https://mfaysal.com/free-tools/).

## 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.*

---

*Originally published on [mfaysal.com](https://mfaysal.com/blog/javascript-double-equals-vs-triple-equals).*

I'm learning cybersecurity on TryHackMe — follow my progress at [tryhackme.com/p/mfaysal.dev](https://tryhackme.com/p/mfaysal.dev).
