How to Compare Values in JavaScript: Equality, Coercion, and Best Practices
Comparisons are at the heart of every decision your code makes. Every if statement, every loop condition, every filter operation relies on comparing values. JavaScript provides two sets of equality operators (== vs ===), four relational operators (>, <, >=, <=), and a special Object.is() method. Each follows different rules, especially when comparing values of different types.
Understanding these rules is not optional. JavaScript's comparison behavior with type coercion is the source of some of the language's most infamous quirks. This guide walks you through every comparison scenario with clear examples, explains the exact coercion rules the engine follows, and shows you why one simple habit eliminates nearly all comparison-related bugs.
Comparison Operators
The relational comparison operators compare two values and return a boolean (true or false).
Greater Than and Less Than
console.log(10 > 5); // true
console.log(5 > 10); // false
console.log(5 > 5); // false (ot greater, equal)
console.log(3 < 8); // true
console.log(8 < 3); // false
console.log(3 < 3); // false (not less, equal)
Greater Than or Equal, Less Than or Equal
console.log(10 >= 10); // true (equal counts)
console.log(10 >= 5); // true (greater counts)
console.log(5 >= 10); // false
console.log(3 <= 3); // true (equal counts)
console.log(3 <= 8); // true (less counts)
console.log(8 <= 3); // false
Practical Usage
let age = 17;
if (age >= 18) {
console.log("Access granted");
} else {
console.log("Access denied"); // This runs
}
let temperature = 38.5;
if (temperature > 37.5) {
console.log("Fever detected"); // This runs
}
let score = 85;
let grade;
if (score >= 90) {
grade = "A";
} else if (score >= 80) {
grade = "B"; // This runs
} else if (score >= 70) {
grade = "C";
} else {
grade = "F";
}
console.log(grade); // "B"
Equality: == vs === (Abstract vs. Strict Equality)
JavaScript has two equality operators, and understanding the difference between them is one of the most important things you will learn about the language.
Strict Equality: ===
The strict equality operator === compares both the value and the type. If the types are different, it immediately returns false without any conversion.
console.log(5 === 5); // true (same type, same value)
console.log(5 === "5"); // false (different types (number vs string))
console.log(0 === false); // false (different types (number vs boolean))
console.log("" === false); // false (different types (string vs boolean))
console.log(null === undefined); // false (different types)
console.log(true === 1); // false (different types)
The behavior of === is predictable and intuitive. Two values are strictly equal only when they are the same type and the same value. There are no surprises.
// What you see is what you get
console.log(42 === 42); // true
console.log("hello" === "hello"); // true
console.log(true === true); // true
console.log(null === null); // true
console.log(undefined === undefined); // true
// Different types = always false
console.log(42 === "42"); // false
console.log(0 === ""); // false
console.log(false === "false"); // false
console.log(false === 0); // false
Abstract (Loose) Equality: ==
The loose equality operator == performs type coercion before comparing. If the types are different, JavaScript converts one or both values to a common type and then compares. The rules it follows are complex and often counterintuitive.
console.log(5 == "5"); // true ("5" converted to 5)
console.log(0 == false); // true (false converted to 0)
console.log("" == false); // true (both convert to 0)
console.log(null == undefined); // true (special rule)
console.log(1 == true); // true (true converted to 1)
console.log("" == 0); // true ("" converted to 0)
The == Coercion Rules (Step by Step)
When the types differ, == applies these rules in order:
Rule 1: null and undefined
null == undefined returns true. Neither null nor undefined equals anything else with ==.
console.log(null == undefined); // true (special rule
console.log(null == null); // true
console.log(undefined == undefined); // true
console.log(null == 0); // false (null only equals undefined
console.log(null == ""); // false
console.log(null == false); // false
console.log(undefined == 0); // false
console.log(undefined == ""); // false
console.log(undefined == false); // false
Rule 2: Number vs. String The string is converted to a number.
console.log(42 == "42"); // true ("42" → 42)
console.log(0 == ""); // true ("" → 0)
console.log(0 == "0"); // true ("0" → 0)
console.log(1 == "01"); // true ("01" → 1)
console.log(100 == "1e2"); // true ("1e2" → 100)
Rule 3: Boolean vs. Anything The boolean is converted to a number first (true → 1, false → 0), then compared again.
console.log(true == 1); // true (true → 1, then 1 == 1)
console.log(true == "1"); // true (true → 1, then 1 == "1" → 1 == 1)
console.log(false == 0); // true (false → 0, then 0 == 0)
console.log(false == ""); // true (false → 0, then 0 == "" → 0 == 0)
console.log(false == "0"); // true (false → 0, then 0 == "0" → 0 == 0)
Rule 4: Object vs. Primitive
The object is converted to a primitive using its valueOf() or toString() method.
console.log([1] == 1); // true ([1].toString() → "1" → 1)
console.log(["5"] == 5); // true (["5"].toString() → "5" → 5)
console.log([null] == ""); // true ([null].toString() → "")
console.log([undefined] == ""); // true ([undefined].toString() → "")
The Chaos of ==
Here are some results that demonstrate why == is unreliable:
// All of these are true with ==
console.log(false == "0"); // true
console.log(false == ""); // true
console.log(false == []); // true
console.log(false == [0]); // true
console.log("" == 0); // true
console.log("" == []); // true
console.log(0 == []); // true
console.log(0 == "0"); // true
// But this breaks transitivity
console.log(false == "0"); // true
console.log(false == ""); // true
console.log("0" == ""); // false! ← If A==B and A==C, shouldn't B==C?
// And this is truly bewildering
console.log([] == ![]); // true!
// Explaination:
// Step 1: ![] = false ([] is truthy, negated to false)
// Step 2: [] == false
// Step 3: [] → "" → 0, false → 0
// Step 4: 0 == 0 → true
Inequality: != vs !==
The inequality operators are the negations of their equality counterparts.
!== (Strict Inequality)
Returns true when === would return false:
console.log(5 !== "5"); // true (different types)
console.log(5 !== 5); // false (same type, same value)
console.log(null !== undefined); // true (different types)
console.log(0 !== false); // true (different types)
!= (Loose Inequality)
Returns true when == would return false. It inherits all the same coercion issues:
console.log(5 != "5"); // false ("5" coerced to 5, 5 == 5)
console.log(0 != false); // false (false coerced to 0, 0 == 0)
console.log(null != undefined); // false (special rule, null == undefined)
console.log("" != 0); // false ("" coerced to 0, 0 == 0)
Just as you should always use === instead of ==, you should always use !== instead of !=. The same coercion problems apply to inequality operators.
Comparing Strings (Lexicographic/Unicode Order)
When both operands are strings, JavaScript compares them character by character using their Unicode code point values, similar to dictionary (lexicographic) ordering.
How String Comparison Works
console.log("apple" < "banana"); // true ('a' (97) < 'b' (98))
console.log("cat" < "dog"); // true ('c' (99) < 'd' (100))
console.log("ant" < "antelope"); // true ("ant" is a prefix, shorter = smaller)
The comparison goes character by character, and the first difference decides the result:
console.log("abc" < "abd"); // true (first two chars equal, 'c' (99) < 'd' (100))
console.log("abc" < "abcd"); // true ("abc" is a prefix of "abcd")
console.log("abcd" < "abc"); // false (longer string is "greater" when it starts the same)
Case Sensitivity
Uppercase letters have lower Unicode values than lowercase letters. This means uppercase comes "before" lowercase in comparisons:
console.log("A" < "a"); // true ('A' is 65, 'a' is 97)
console.log("Z" < "a"); // true ('Z' is 90, 'a' is 97)
console.log("Banana" < "apple"); // true ('B' (66) < 'a' (97))
This is often surprising. In a dictionary, "apple" comes before "Banana", but in JavaScript's Unicode comparison, any uppercase letter comes before any lowercase letter.
Common Character Code Points
| Character | Unicode Value |
|---|---|
0 - 9 | 48 - 57 |
A - Z | 65 - 90 |
a - z | 97 - 122 |
This means digits come before uppercase letters, which come before lowercase:
console.log("9" < "A"); // true (57 < 65)
console.log("Z" < "a"); // true (90 < 97)
console.log("9" < "a"); // true (57 < 97)
Comparing Numeric Strings
String comparison is character-by-character, not numeric:
console.log("9" > "10"); // true! ('9' (57) > '1' (49), comparison stops)
console.log("100" < "20"); // true! ('1' (49) < '2' (50))
console.log("100" < "99"); // true! ('1' (49) < '9' (57))
If you need to compare numbers that happen to be strings, convert them to numbers first:
console.log(Number("9") > Number("10")); // false (9 > 10 is false)
console.log(Number("100") < Number("20")); // false (100 < 20 is false)
Locale-Aware Comparison: localeCompare()
For proper alphabetical sorting that respects language rules, use localeCompare():
// Unicode comparison (incorrect for sorting)
console.log("é" > "z"); // false in some encodings, true in others
// Locale-aware comparison (correct for sorting)
console.log("é".localeCompare("z")); // -1 (é comes before z in French)
// Case-insensitive comparison
console.log("Apple".localeCompare("apple", undefined, { sensitivity: "base" }));
// 0 (treated as equal
// Sorting an array alphabetically
let fruits = ["Banana", "apple", "Cherry"];
fruits.sort();
console.log(fruits); // ["Banana", "Cherry", "apple"] (uppercase first!)
fruits.sort((a, b) => a. localeCompare(b));
console.log(fruits); // ["apple", "Banana", "Cherry"] (proper alphabetical)
Comparing Different Types: Coercion Rules
When comparison operators (>, <, >=, <=) encounter different types, they follow specific conversion rules.
Number vs. String
Strings are converted to numbers:
console.log(10 > "5"); // true ("5" → 5, then 10 > 5)
console.log("10" > 5); // true ("10" → 10, then 10 > 5)
console.log("10" > "9"); // false (both strings, compared as strings! '1' < '9')
When both operands are strings, no numeric conversion happens. They are compared lexicographically. When one is a number, the string is converted to a number. This distinction catches many developers:
// Both strings: lexicographic comparison
console.log("10" > "9"); // false (character '1' < character '9')
// One number: numeric comparison
console.log(10 > "9"); // true ("9" → 9, then 10 > 9)
console.log("10" > 9); // true ("10" → 10, then 10 > 9)
Boolean in Comparisons
Booleans are converted to numbers (true → 1, false → 0):
console.log(true > 0); // true (1 > 0)
console.log(true > false); // true (1 > 0)
console.log(false < 1); // true (0 < 1)
console.log(true >= 1); // true (1 >= 1)
console.log(false <= 0); // true (0 <= 0)
Comparing with NaN
Any comparison with NaN returns false, including comparing NaN with itself:
console.log(NaN > 0); // false
console.log(NaN < 0); // false
console.log(NaN >= 0); // false
console.log(NaN <= 0); // false
console.log(NaN == 0); // false
console.log(NaN == NaN); // false (NaN is not equal to anything!)
console.log(NaN === NaN); // false (still not equal with strict equality)
// Even inequality returns true
console.log(NaN != NaN); // true
console.log(NaN !== NaN); // true
// Only reliable way to check for NaN
console.log(Number.isNaN(NaN)); // true
This means comparisons with strings that cannot be converted to numbers always return false:
console.log("hello" > 0); // false ("hello" → NaN, NaN > 0 is false
console.log("hello" < 0); // false (NaN < 0 is also false
console.log("hello" == 0); // false (NaN == 0 is false
Comparing with null and undefined (Special Cases)
The comparison behavior of null and undefined is one of the most inconsistent and confusing areas of JavaScript. The == operator and the relational operators (>, <, >=, <=) follow different rules for these values.
null vs. undefined with ==
console.log(null == undefined); // true (special rule in the spec
console.log(null === undefined); // false (different types
This is one of the few accepted uses of ==. The expression value == null is a convenient way to check for both null and undefined at once.
null with Relational Operators
For relational operators, null is converted to 0:
console.log(null > 0); // false (0 > 0 is false
console.log(null < 0); // false (0 < 0 is false
console.log(null >= 0); // true (0 >= 0 is true
console.log(null <= 0); // true (0 <= 0 is true
console.log(null == 0); // false (special rule! null only equals undefined
This is deeply inconsistent. null >= 0 is true and null <= 0 is true, which logically implies null == 0 should be true. But it is false because == applies its own special rules for null instead of using numeric conversion.
// The inconsistency summarized:
console.log(null >= 0); // true (null converts to 0 for comparison
console.log(null == 0); // false (null uses special equality rules
console.log(null > 0); // false (0 > 0 is false
// So null is >= 0, but not > 0, and not == 0. This makes no mathematical sense.
undefined with Relational Operators
undefined is converted to NaN for relational comparisons, and NaN makes everything false:
console.log(undefined > 0); // false (NaN > 0
console.log(undefined < 0); // false (NaN < 0
console.log(undefined >= 0); // false (NaN >= 0
console.log(undefined <= 0); // false (NaN <= 0
console.log(undefined == 0); // false (undefined only equals null
console.log(undefined == null); // true (special rule
The Lesson
// DANGEROUS: comparing null or undefined with relational operators
let value = null;
if (value >= 0) {
console.log("Non-negative!"); // This runs! But value is null, not a number.
}
// SAFE: check for null/undefined explicitly before comparing
let value = null;
if (value != null && value >= 0) {
console.log("Non-negative!"); // Does NOT run (null is caught first
}
// Or use strict checks
if (value !== null && value !== undefined && value >= 0) {
console.log("Non-negative!");
}
Never use relational operators (>, <, >=, <=) with values that might be null or undefined. The results are inconsistent and unpredictable. Always check for null/undefined first, then perform the comparison on the guaranteed numeric value.
Object.is(): The Strictest Comparison
Object.is() was introduced in ES6 as a comparison method that is even stricter than ===. It behaves identically to === in almost all cases, with exactly two differences.
Difference 1: NaN Equals NaN
// === says NaN is not equal to NaN
console.log(NaN === NaN); // false
// Object.is() says NaN IS equal to NaN
console.log(Object.is(NaN, NaN)); // true
Difference 2: +0 and -0 Are Different
// === says +0 and -0 are equal
console.log(0 === -0); // true
console.log(+0 === -0); // true
// Object.is() says they are different
console.log(Object.is(0, -0)); // false
console.log(Object.is(+0, -0)); // false
console.log(Object.is(0, 0)); // true
console.log(Object.is(-0, -0)); // true
Everything Else Is the Same
console.log(Object.is(42, 42)); // true (same as ===
console.log(Object.is("hello", "hello")); // true
console.log(Object.is(42, "42")); // false (no coercion, same as ===
console.log(Object.is(null, null)); // true
console.log(Object.is(undefined, undefined)); // true
console.log(Object.is(null, undefined)); // false (same as ===
let obj = {};
console.log(Object.is(obj, obj)); // true (same reference
console.log(Object.is({}, {})); // false (different objects
Complete Comparison of All Three
| Expression | == | === | Object.is() |
|---|---|---|---|
5, 5 | true | true | true |
5, "5" | true | false | false |
0, false | true | false | false |
"", false | true | false | false |
null, undefined | true | false | false |
NaN, NaN | false | false | true |
+0, -0 | true | true | false |
[], false | true | false | false |
"0", false | true | false | false |
When to Use Object.is()
In everyday code, you almost never need Object.is(). It is useful in specific situations:
// Checking for NaN (alternative to Number.isNaN)
if (Object.is(value, NaN)) {
console.log("Value is NaN");
}
// Distinguishing +0 and -0 (rare mathematical computations)
if (Object.is(result, -0)) {
console.log("Result is negative zero");
}
For all other comparisons, === is the right choice.
Golden Rule: Always Use === Unless You Have a Specific Reason
After seeing all the coercion rules and edge cases, the practical advice is simple.
The Rule
Use === and !== for all comparisons. Do not use == or !=.
// ALWAYS do this
if (value === 42) { }
if (name !== "") { }
if (status === null) { }
if (count !== undefined) { }
// NEVER do this (in almost all cases)
if (value == 42) { }
if (name != "") { }
The One Exception
The only widely accepted use of == is checking for null or undefined simultaneously:
// This is the accepted exception
if (value == null) {
// Handles both null AND undefined
console.log("Value is null or undefined");
}
// It's equivalent to:
if (value === null || value === undefined) {
console.log("Value is null or undefined");
}
This works because null == undefined is true, and null does not loosely equal anything else (null == 0 is false, null == "" is false, null == false is false). So value == null precisely catches null and undefined without false positives.
// value == null catches exactly two values:
console.log(null == null); // true ✓
console.log(undefined == null); // true ✓
console.log(0 == null); // false (not caught
console.log("" == null); // false (not caught
console.log(false == null); // false (not caught
console.log(NaN == null); // false (not caught
Why This Matters
Using === eliminates an entire category of bugs. You never have to think about coercion rules, never have to wonder whether "" will equal 0, and never have to debug behavior that changes based on the types of the values being compared.
// With ===, what you see is what you get
let input = "";
if (input === 0) {
console.log("Is zero"); // Does NOT run ("" is not 0)
}
if (input === false) {
console.log("Is false"); // Does NOT run ("" is not false)
}
if (input === "") {
console.log("Is empty string"); // Runs (exact match)
}
// With ==, you get confusing results
if (input == 0) {
console.log("Is zero??"); // Runs! "" == 0 is true
}
if (input == false) {
console.log("Is false??"); // Runs! "" == false is true
}
Configure ESLint
Most ESLint configurations include the eqeqeq rule, which enforces === and !==:
{
"rules": {
"eqeqeq": ["error", "always", { "null": "ignore" }]
}
}
The "null": "ignore" option allows == null as the accepted exception while flagging all other uses of ==.
Summary
Comparisons are fundamental to every program, and JavaScript's comparison rules have important nuances:
- Relational operators (
>,<,>=,<=) convert operands to numbers when types differ, except when both are strings (lexicographic comparison). - String comparison is character-by-character using Unicode code points. Uppercase letters come before lowercase. Numeric strings are compared character-by-character, not numerically. Use
localeCompare()for proper alphabetical sorting. ===(strict equality) compares value and type with no coercion. It is predictable and should be your default.==(loose equality) performs type coercion following complex rules. It produces counterintuitive results like"" == 0(true),[] == false(true), and[] == .!==should be used instead of!=for the same reasons.- NaN is not equal to anything, including itself, with both
==and===. UseNumber.isNaN()to check for it. - null and undefined are loosely equal to each other with
==but not strictly equal with===. Relational operators convertnullto0andundefinedtoNaN, creating inconsistent results. Object.is()is the strictest comparison. It differs from===in two cases:NaNequalsNaN, and+0does not equal-0.- Always use
===and!==. The only common exception isvalue == nullto check for bothnullandundefinedsimultaneously. - Never use relational operators on values that might be
nullorundefinedwithout checking first.
With a solid understanding of comparisons, you are ready to put them to work in conditional branching with if, else, and the ternary operator.