Comparison and Logical Operators

7 min read

The condition inside an if (...) is built from two kinds of operators: comparison operators that ask "is this value bigger, smaller, or equal to that one?" and logical operators that combine those answers with AND, OR, and NOT. Used well they keep test assertions sharp; used carelessly they introduce some of the slipperiest bugs in JavaScript. This lesson covers both, with the type-coercion trap that has burned every QA engineer at least once.

Comparison operators

The six basic comparisons:

console.log(200 === 200);   // true   — strict equal
console.log(200 !== 404);   // true   — strict not-equal
console.log(2000 > 1500);   // true   — greater than
console.log(2000 < 1500);   // false  — less than
console.log(1500 >= 1500);  // true   — greater than or equal
console.log(1500 <= 1499);  // false  — less than or equal

Each one returns a boolean (true or false). That's the value if is checking.

=== vs == — always use ===

JavaScript has two equality operators. They look almost identical but behave differently.

  • === (strict equality) compares value AND type. Two values are only equal if both match.
  • == (loose equality) coerces types before comparing. "200" == 200 is true because the string "200" gets converted to the number 200 first.

That coercion is exactly the kind of behaviour that hides bugs in tests:

const apiStatus = "200";    // string from a JSON parse
const expected = 200;       // number
 
console.log(apiStatus == expected);   // true   — looks correct, isn't
console.log(apiStatus === expected);  // false  — type mismatch caught

If your test uses == and the API one day starts returning the status as a string instead of a number, the test passes silently. With ===, the test fails the moment the contract changes — exactly when you want to know.

== vs === — same value, different rules

== (loose equality)

  • "200" == 200

    true — string coerced to number

  • 0 == false

    true — false coerced to 0

  • null == undefined

    true — special-case rule

  • " " == 0

    true — string trims and coerces

  • Hides type-mismatch bugs

=== (strict equality)

  • "200" === 200

    false — different types

  • 0 === false

    false — number vs boolean

  • null === undefined

    false — different values

  • " " === 0

    false — string vs number

  • Catches type drift early

A useful habit: ban == from your code entirely. ESLint has a built-in rule (eqeqeq) that flags every ==. Turn it on and forget the operator exists.

Logical operators

Three operators combine boolean expressions.

  • && (AND) — true only if BOTH sides are true.
  • || (OR) — true if EITHER side is true.
  • ! (NOT) — flips a boolean: !true is false.
const statusOk = true;
const fastEnough = true;
const hasUser = false;
 
console.log(statusOk && fastEnough);   // true
console.log(statusOk && hasUser);      // false
console.log(statusOk || hasUser);      // true   — only one needs to be true
console.log(!hasUser);                 // true   — negation

AND for "all conditions must hold"

Use && when every requirement must be met. A real assertion: the request succeeded AND it came back fast enough.

const statusCode = 200;
const responseTime = 1450;
 
if (statusCode === 200 && responseTime < 2000) {
  console.log("✅ Healthy response");
} else {
  console.log("❌ Something is off");
}

Output:

✅ Healthy response

If either condition fails, the whole expression is false. JavaScript also short-circuits — if the first part is false, it doesn't bother evaluating the second. That can matter for performance, and occasionally for safety (obj && obj.field won't throw if obj is null).

OR for "any of several values is fine"

Use || when any of several values would be acceptable. Many APIs return either 200 or 201 for a successful POST, depending on whether the resource was new or updated.

const statusCode = 201;
 
if (statusCode === 200 || statusCode === 201) {
  console.log("✅ Resource saved");
} else {
  console.log("❌ Save failed");
}

Output:

✅ Resource saved

NOT for negation

! flips a boolean — useful when the cleaner reading is "not this":

const isError = false;
 
if (!isError) {
  console.log("Continuing");
}

Output:

Continuing

Two ! in a row (!!value) coerces any value to a real boolean — sometimes useful for assertions like expect(!!response.body).toBe(true).

Truthy and falsy values

Inside an if (...), JavaScript also accepts non-boolean values and decides whether each one counts as "true" or "false". The full list of falsy values is small and worth memorising:

  • false
  • 0
  • "" (empty string)
  • null
  • undefined
  • NaN

Everything else — including "0" (a string), [] (an empty array), and {} (an empty object) — is truthy. That's how if (response.body) works as a "does the body have anything in it?" check: an empty string is falsy, so the if skips it; any non-empty body is truthy and the block runs.

const responseBody = "";
if (responseBody) {
  console.log("Got a body:", responseBody);
} else {
  console.log("Empty body — skipping further checks");
}

Output:

Empty body — skipping further checks

A subtle gotcha: [] and {} are truthy. if (errors) where errors = [] runs the block — even though the array is empty. Use errors.length > 0 if you actually mean "the array has items."

A complete API-response validation

Combining everything:

const response = {
  statusCode: 200,
  time: 1450,
  body: { userId: 42 }
};
const slaMs = 2000;
 
const statusOk = response.statusCode === 200 || response.statusCode === 201;
const fastEnough = response.time < slaMs;
const hasUserId = !!response.body.userId;
 
if (statusOk && fastEnough && hasUserId) {
  console.log("✅ All checks passed");
} else {
  console.log("❌ Failed:", { statusOk, fastEnough, hasUserId });
}

Output:

✅ All checks passed

Each check is a named boolean, then they're combined with &&. When one fails, the diagnostic in the else tells you exactly which — far more useful than a generic "test failed" message.

⚠️ Common mistakes

  • Using == accidentally. It's one keystroke shorter than === and the bugs it causes are silent. Configure ESLint's eqeqeq rule and let the linter shout at you.
  • Mixing up && and ||. "Either A or B" sometimes really means "B is fine if A failed" — say, "expect 200 OR 201". But "200 AND fast" needs both. Read each condition aloud as English ("both", "either", "neither") and the operator usually picks itself.
  • Treating empty arrays as falsy. if (errors) where errors is [] runs the block. Use errors.length > 0 for "has items," errors.length === 0 for "no items."

🎯 Practice task

Build a request validator. 15-20 minutes.

  1. In your js-for-qa folder, create validate.js.

  2. Declare a fake response object:

    const response = {
      statusCode: 200,
      time: 1450,
      body: { userId: 42, email: "alice@example.com" }
    };
  3. Compute three named booleans:

    • statusOk — true if statusCode is 200 OR 201.
    • fastEnough — true if time is under 2000ms.
    • hasUser — truthy if body.userId exists.
  4. Use && to assert all three are true. If any fails, print the { statusOk, fastEnough, hasUser } object so you can see which one was the culprit.

  5. Run node validate.js and confirm "all passed."

  6. Stretch: change statusCode to "200" (a string). Without changing your assertions, rerun and explain why statusOk now becomes false. (Hint: your code uses ===.)

The next lesson covers switch and the ternary ?: operator — two ways to express decisions more concisely than if/else.

// tip to track lessons you complete and pick up where you left off across devices.