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 equalEach 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" == 200istruebecause the string"200"gets converted to the number200first.
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 caughtIf 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:!trueisfalse.
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 — negationAND 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:
false0""(empty string)nullundefinedNaN
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'seqeqeqrule 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)whereerrorsis[]runs the block. Useerrors.length > 0for "has items,"errors.length === 0for "no items."
🎯 Practice task
Build a request validator. 15-20 minutes.
-
In your
js-for-qafolder, createvalidate.js. -
Declare a fake response object:
const response = { statusCode: 200, time: 1450, body: { userId: 42, email: "alice@example.com" } }; -
Compute three named booleans:
statusOk— true ifstatusCodeis 200 OR 201.fastEnough— true iftimeis under 2000ms.hasUser— truthy ifbody.userIdexists.
-
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. -
Run
node validate.jsand confirm "all passed." -
Stretch: change
statusCodeto"200"(a string). Without changing your assertions, rerun and explain whystatusOknow becomesfalse. (Hint: your code uses===.)
The next lesson covers switch and the ternary ?: operator — two ways to express decisions more concisely than if/else.