Skip to main content

βš–οΈ Comparison and Logical Operators

Every "if" your program has ever run started with a comparison. This lesson gives you the operators that ask questions β€” is this equal to that? is either true? β€” and the crucial habit of using === so JavaScript never surprises you with a silent type conversion.

🎯 Learning Objectives

By the end of this lesson, you will be able to:

  • Explain the difference between strict (===) and loose (==) equality and default to strict
  • Predict the result of relational and string comparisons, including lexicographic ordering
  • Describe JavaScript's truthy/falsy values and use them safely
  • Use short-circuit evaluation with && and || for guards and defaults
  • Choose between || and the nullish coalescing operator ??, and write clean ternary expressions

Estimated Time: 30–40 minutes  β€’  Difficulty: Beginner

Hands-on: Build a password-strength checker driven entirely by comparison and logical operators.

In This Lesson

Operators That Ask Questions

Arithmetic operators produce values. Comparison and logical operators evaluate them β€” they answer yes/no questions and return a Boolean (true or false). Together they are the engine behind every if, every loop condition, and every filter.

graph TD A[Decision Making] A --> B[Comparison] A --> C[Logical] B --> B1["Equality<br/>=== !== == !="] B --> B2["Relational<br/>> < >= <="] C --> C1["AND &&"] C --> C2["OR ||"] C --> C3["NOT !"] C --> C4["Nullish ??"]
πŸ’‘ Analogy β€” the doorman. A club doorman checks IDs (comparison) and enforces several rules at once: "over 21 and on the guest list, or a member" (logical). Comparison and logical operators are how your code plays doorman over data.

Equality: === vs. ==

JavaScript has two equality operators, and the difference between them is one of the most important things a beginner learns.

OperatorNameComparesExample
===Strict equalityValue and type β€” no conversion5 === "5" β†’ false
==Loose equalityValue only β€” converts types first5 == "5" β†’ true
!==Strict inequalityValue or type differs5 !== "5" β†’ true
!=Loose inequalityValue differs after conversion5 != "5" β†’ false
// Loose (==) converts before comparing β€” the source of many bugs
console.log(5 == '5');            // true  (string β†’ number)
console.log(0 == false);         // true  (false β†’ 0)
console.log('' == 0);            // true  ('' β†’ 0)
console.log(null == undefined);  // true  (special case)

// Strict (===) never converts β€” what you see is what you get
console.log(5 === '5');           // false (number vs string)
console.log(0 === false);         // false (number vs boolean)
console.log(null === undefined);  // false (different types)
πŸ’‘ Analogy β€” two passport checks. Loose equality is a guard who only confirms you're the right person and ignores which kind of passport you carry. Strict equality checks the person and the passport type. The stricter check is the safer one.

βœ… Rule of thumb: always use ===

Professional JavaScript defaults to === and !==. Loose == triggers a set of conversion rules that are hard to remember and easy to get wrong. The one common, deliberate use of loose equality is x == null, which conveniently matches both null and undefined β€” but even that is a stylistic choice.

Relational & String Comparison

The relational operators >, <, >=, and <= compare magnitude and return a Boolean. With numbers they behave exactly as you'd expect; with strings they compare lexicographically β€” character by character using Unicode code points.

// Numeric comparison
console.log(10 > 5);    // true
console.log(5 >= 5);    // true
console.log('10' > 5);  // true β€” the string '10' is coerced to number 10

// String comparison is dictionary-style, NOT numeric
console.log('apple' < 'banana'); // true  ('a' < 'b')
console.log('Apple' < 'apple');  // true  (uppercase code points come first)
console.log('10' < '9');         // true  ('1' < '9', so '10' sorts before '9'!)

⚠️ The classic sorting bug

'10' < '9' is true because comparison stops at the first character. This is why [1, 2, 10].sort() returns [1, 10, 2] β€” sort() compares stringified values by default. For numbers, always pass a comparator: arr.sort((a, b) => a - b).

For human-facing sorting of text β€” names, cities, anything with accents β€” use localeCompare(), which respects language rules:

const names = ['ZΓΌrich', 'Γ„pfel', 'Andres'];
names.sort((a, b) => a.localeCompare(b, 'de'));
console.log(names); // correct German ordering

// A tidy case-insensitive equality helper
const equalsIgnoreCase = (a, b) =>
  a.localeCompare(b, undefined, { sensitivity: 'accent' }) === 0;
console.log(equalsIgnoreCase('Hello', 'hello')); // true

πŸ“– Note β€” objects compare by reference

{a: 1} === {a: 1} is false. Objects and arrays are compared by identity (are they the same object in memory?), not by contents. Two separately-created objects are never ===, even with identical data. To compare contents you must walk their properties or use a library helper.

Truthy, Falsy & Booleans

Logical operators don't require actual Booleans β€” they work on the truthiness of any value. It is quicker to memorise the short list of falsy values; everything else is truthy.

⚠️ The eight falsy values

false, 0, -0, 0n (BigInt zero), "" (empty string), null, undefined, and NaN. That's it. Everything else β€” including "0", "false", [], and {} β€” is truthy.

// The double-NOT idiom converts any value to a real boolean
console.log(!!'hello'); // true
console.log(!!0);       // false
console.log(!![]);      // true  β€” an empty array is truthy!
console.log(!!'');      // false

This matters because the surprising ones β€” [] is truthy, "0" is truthy β€” cause real bugs when used as conditions. When you specifically mean "is this array empty?", test arr.length === 0, not !arr.

Logical Operators & Short-Circuiting

The three core logical operators combine or invert conditions:

OperatorNameReturns
&&ANDTruthy only if both sides are truthy
||ORTruthy if either side is truthy
!NOTInverts a value to the opposite Boolean
πŸ’‘ Analogy β€” circuits. && is a series circuit: current flows only if every switch is on. || is a parallel circuit: current flows if any switch is on. ! is an inverter that flips on to off.

Short-circuit evaluation

JavaScript evaluates left to right and stops as soon as the answer is certain. && stops at the first falsy operand; || stops at the first truthy one. Crucially, they return the operand itself, not a coerced Boolean β€” which enables two everyday patterns:

// Guard: only read .name if user exists (avoids a TypeError)
const user = null;
console.log(user && user.name); // null β€” the right side is never touched

// Default: fall back when the left side is falsy
const input = '';
const label = input || 'Untitled';
console.log(label); // 'Untitled'

// Returning the operand, not just true/false
console.log('Hello' && 'World'); // 'World' (last truthy value)
console.log('' || 'Fallback');   // 'Fallback' (first truthy value)
Short-circuit evaluation of AND and OR Logical AND stops at the first falsy operand; logical OR stops at the first truthy operand, skipping the rest. && stops at first FALSY truthy FALSY βœ‹ skipped || stops at first TRUTHY falsy TRUTHY βœ‹ skipped
Figure 1 β€” Short-circuiting: once the result is decided, the remaining operands are never evaluated. This is why the right side can safely be a function call or property access.

πŸ” Worked example β€” a permission check

Real access rules combine several conditions. Grouping with parentheses makes intent unmistakable:

function canEnter({ age, hasTicket, isMember }) {
  // Members always get in; everyone else needs to be 18+ with a ticket.
  return isMember || (age >= 18 && hasTicket);
}

console.log(canEnter({ age: 25, hasTicket: true,  isMember: false })); // true
console.log(canEnter({ age: 16, hasTicket: true,  isMember: false })); // false
console.log(canEnter({ age: 16, hasTicket: false, isMember: true  })); // true

Nullish Coalescing (??)

Using || for default values has a subtle flaw: it replaces every falsy value, including legitimate ones like 0, false, and "". The nullish coalescing operator ?? fixes this by falling back only when the left side is null or undefined.

// || treats 0 and '' as "missing" β€” often wrong
console.log(0  || 'default');  // 'default'  ← 0 was a valid value!
console.log('' || 'default');  // 'default'

// ?? only falls back for null / undefined
console.log(0  ?? 'default');  // 0          ← preserved
console.log('' ?? 'default');  // ''         ← preserved
console.log(null ?? 'default'); // 'default'
console.log(undefined ?? 'x');  // 'x'
πŸ’‘ Analogy β€” the backup generator. ?? is a generator that only kicks in when the main power is completely out (null/undefined). A mere flicker β€” a 0 or an empty string β€” is still real power, so the generator stays off.

?? pairs naturally with optional chaining (?.), which safely reads deep properties that might not exist:

const user = { name: 'Alice', address: { city: 'New York' } };

// Old, verbose safe access
let country;
if (user && user.address && user.address.country) {
  country = user.address.country;
} else {
  country = 'Unknown';
}

// Modern one-liner
const country2 = user?.address?.country ?? 'Unknown';
console.log(country2); // 'Unknown'

⚠️ Don't mix ?? with &&/|| unparenthesised

JavaScript makes a ?? b || c a syntax error on purpose, to force you to be explicit. Write (a ?? b) || c or a ?? (b || c) so the intent is unambiguous.

The Ternary Operator

The conditional (ternary) operator is the only JavaScript operator that takes three operands: condition ? valueIfTrue : valueIfFalse. It is an expression, so unlike an if statement it produces a value you can assign or return.

const age = 20;

// Instead of a four-line if/else...
const status = age >= 18 ? 'Adult' : 'Minor';
console.log(status); // 'Adult'

// Chained ternaries read like a lookup table (keep them shallow)
function greetingFor(hour) {
  return hour < 12 ? 'Good morning'
       : hour < 18 ? 'Good afternoon'
       : 'Good evening';
}
console.log(greetingFor(9));  // 'Good morning'
console.log(greetingFor(20)); // 'Good evening'
πŸ’‘ Analogy β€” a signposted fork. The ternary is a fork in the road with one clear sign: true goes left, false goes right. Both paths rejoin at the next line β€” you just pick up a different value along the way.

⚠️ When to stop

Ternaries are perfect for choosing between two values. Once you're nesting three or more, or running side effects in each branch, a plain if or a switch reads far better. Prefer clarity over cleverness.

Hands-on Exercise

πŸ‹οΈ Build a password-strength checker

Objective: Write checkPassword(pw) that returns an object of { valid, errors, strength } using comparison and logical operators.

Rules:

  1. Reject a missing or non-string password up front.
  2. Collect an errors array: at least 8 characters, at least one uppercase, one lowercase, one digit, and one symbol.
  3. valid is true only when errors is empty.
  4. If valid, rate strength as "medium", "strong", or "very strong" based on bonus factors (length β‰₯ 12, multiple digits, multiple symbols).
πŸ’‘ Hint

Use /[A-Z]/.test(pw) style regex tests for each character class. Count how many bonus conditions are met with [cond1, cond2, cond3].filter(Boolean).length, then map that count to a label with a ternary.

βœ… Sample solution
function checkPassword(pw) {
  const result = { valid: false, errors: [], strength: 'weak' };

  if (typeof pw !== 'string' || pw.length === 0) {
    result.errors.push('Password is required');
    return result;
  }

  if (pw.length < 8)          result.errors.push('At least 8 characters');
  if (!/[A-Z]/.test(pw))      result.errors.push('One uppercase letter');
  if (!/[a-z]/.test(pw))      result.errors.push('One lowercase letter');
  if (!/\d/.test(pw))         result.errors.push('One number');
  if (!/[^A-Za-z0-9]/.test(pw)) result.errors.push('One symbol');

  result.valid = result.errors.length === 0;

  if (result.valid) {
    const bonuses = [
      pw.length >= 12,
      (pw.match(/\d/g) || []).length >= 3,
      (pw.match(/[^A-Za-z0-9]/g) || []).length >= 2
    ].filter(Boolean).length;

    result.strength = bonuses === 0 ? 'medium'
                    : bonuses === 1 ? 'strong'
                    : 'very strong';
  }
  return result;
}

console.log(checkPassword('abc'));          // valid:false, several errors
console.log(checkPassword('Passw0rd!'));    // valid:true,  strength:'medium'
console.log(checkPassword('Sup3r$ecret99')); // valid:true,  strength:'very strong'

🎯 Quick Quiz

Question 1: What does 0 === false evaluate to?

Question 2: Which operator preserves 0 and "" as valid values when supplying a default?

Question 3: Why does '10' < '9' return true?

Summary & Quiz

πŸŽ‰ Key Takeaways

  • Default to strict ===/!==; loose == converts types and hides bugs.
  • Strings compare lexicographically β€” use a numeric comparator for numbers and localeCompare for human text.
  • Only eight values are falsy; [] and "0" are truthy.
  • && and || short-circuit and return an operand, powering guards and defaults.
  • Use ?? (not ||) when 0/""/false are valid, and keep ternaries shallow.

πŸ“š Further Reading

πŸš€ What's Next?

You've now met arithmetic, comparison, and logical operators. In Operator Precedence and Expressions you'll learn the rules that decide which operator runs first when you combine them β€” and how to keep complex expressions readable.

πŸŽ‰ Great progress!

Your code can now make decisions. Next, we untangle how those decisions are ordered.