Topics

Jump to a topic.

Search questions

Find a question by title, tag, or summary.

How does == decide whether two values are equal?

Questions in this chapter (5)
  • Draft
  • medium
  • Askedalmost always
  • Verified October 2026

Answer

== evaluates the spec operation IsLooselyEqual(x, y), which tries rules in order and stops at the first that applies:

  1. Same type → compare as === does (so NaN == NaN is false).
  2. null and undefined are equal to each other, and to nothing else.
  3. Number and String → convert the string with ToNumber, then compare again.
  4. BigInt and String → parse the string as a BigInt; if it is not a valid BigInt literal, the result is false.
  5. Boolean on either side → convert it to a Number (true → 1, false → 0), then compare again.
  6. Object vs String, Number, BigInt or Symbol → convert the object with ToPrimitive (no hint, so valueOf first for ordinary objects), then compare again.
  7. BigInt and Number → equal if they represent the same mathematical value.
  8. Anything else → false.

Each conversion step returns to the top of the algorithm, so one comparison can convert several times: "1" == true becomes "1" == 1, then 1 == 1.

Notable consequences: null == 0 is false (rule 2 consumes null before any numeric conversion), NaN is never equal to anything, and two objects compare by identity because they have the same type. Not transitive: "0" == 0 and 0 == "" are true, but "0" == "" is false (same type, so strict string comparison).

Default to ===. The widely accepted exception is x == null, which tests for null or undefined in one check.

Example

console.log("1" == 1); // → true   (String → Number)
console.log(true == 1); // → true   (Boolean → Number)
console.log("1" == true); // → true   ("1" == 1, then 1 == 1)
console.log("true" == true); // → false  (NaN == 1)
console.log(null == 0); // → false  (null only equals undefined)
console.log(null == undefined); // → true
console.log(NaN == NaN); // → false
console.log([1] == 1); // → true   ([1] → "1" → 1)
console.log([] == false); // → true   (false → 0, [] → "" → 0)
console.log([] == ![]); // → true   (![] is false)
console.log(1n == "1"); // → true
console.log("0" == "");  // → false  (same type: strict string compare)

Pitfalls

  • Thinking == compares by "truthiness". "true" == true is false because true becomes 1 and "true" becomes NaN; truthiness is never consulted.
  • Expecting null == 0 or undefined == 0 to be true. Only null and undefined equal each other.
  • Assuming == is transitive. "0" == 0, 0 == "", but "0" != "".
  • Forgetting the object rule: [] == "", [0] == false, and ({}) == "[object Object]" are all true through ToPrimitive.

Follow-ups

Why is x == null acceptable when == is otherwise avoided?
Rules 2 and 3 of the algorithm mean only null and undefined satisfy it. It reads as "is nullish" and has no coercion surprises. (Legacy: document.all also matches, via Annex B [[IsHTMLDDA]].)
Does == ever throw?
Yes: converting an object calls ToPrimitive, which can run user code or throw a TypeError. Symbols are never converted to Numbers by ==, so Symbol() == 1 is false, not an error.
Does 1n == 1 mean BigInt and Number are the same type?
No. Rule 7 compares their mathematical values. 1n === 1 is false because the types differ.

Deep dive

How it works

The spec text (ECMA-262, IsLooselyEqual) in order:

1. If SameType(x, y): return IsStrictlyEqual(x, y).
2. null/undefined in either order: return true.
   (Annex B, browsers: [[IsHTMLDDA]] object vs null/undefined: true.)
3. Number vs String: compare x with ToNumber(y) (or the mirror).
4. BigInt vs String: n = StringToBigInt(string); undefined → false; else compare.
5. Boolean x: compare ToNumber(x) with y. Boolean y: likewise.
6. String/Number/BigInt/Symbol vs Object: compare with ToPrimitive(object).
7. BigInt vs finite Number: equal iff mathematical values are equal.
8. Return false.

Two properties follow. First, Boolean conversion happens before object conversion: in [] == false, false becomes 0 first, and only then is [] converted ([] → "" → 0). Second, ToPrimitive here is called with no hint, so it behaves like the number hint unless the object defines Symbol.toPrimitive (Date treats no hint as string; see the object-to-primitive question).

Edge cases

Number strings follow StringToNumber: surrounding whitespace is ignored, an empty or whitespace-only string is 0, and "0x10" is 16, but "1_0" and "10n" are NaN.

console.log(" 12\n" == 12); // → true
console.log("" == 0); // → true
console.log("0x10" == 16); // → true
console.log("1_0" == 10); // → false
console.log(2n == "2"); // → true
console.log(2n == "2.5"); // → false (not a valid BigInt string)
console.log(Symbol.iterator == Symbol.iterator); // → true (same type)
console.log(Object("a") == "a"); // → true (wrapper → primitive)
console.log(Object("a") == Object("a")); // → false (two objects, identity)

Large values: a BigInt and a Number compare by exact mathematical value, so 2n ** 53n + 1n == 2 ** 53 is false even though Number(2n ** 53n + 1n) === 2 ** 53. NaN and the infinities are never equal to any BigInt.

Real-world scenario

Form values and query parameters arrive as strings, so req.query.page == 1 "works" until page is " " (whitespace → 0) or "0x1" (→ 1). Parse deliberately instead:

const raw = "0x1";
console.log(raw == 1); // → true (surprising)
const page = Number.parseInt(raw, 10);
console.log(page, page === 1); // → 0 false

Trade-offs

== saves a cast when one operand's type is known, but readers must run the eight rules in their head. === is a single, local rule, and linters (eqeqeq, usually with a null: "ignore" option) enforce it. Use == only for the x == null idiom.

Exercises

Predict each result

console.log([0] == false, "" == [], null == false, undefined == null, "b" + "a" + +"a" == "baNaN");

Non-transitivity

const a = "0", b = 0, c = "";
console.log(a == b, b == c, a == c);