Thesis (short answer): Use truthiness checks for “is anything here?”, use strict comparisons (===, !==, <, >) for exact matches or numeric ranges, prefer guard clauses (early returns) to keep branches simple, and only refactor to switch or a lookup object when you’re matching the same normalized key against many fixed outcomes. The example below shows these choices in a copy‑able validation function and explains why each decision helps avoid common failures.
Alex Carter is the editorial name responsible for Vandutz Academy’s technical direction; the site does not claim a specific degree, employer, certification, or public profile.
Quick reproducible validation example (copy, run, inspect)
This function is intentionally explicit: it normalizes inputs once, uses guard clauses, and mixes truthiness and strict checks where appropriate. Run it in a browser console or Node to reproduce the behaviors described below.
function validateSignup(form = {}) {
// Normalize once
const username = String(form.username ?? "").trim(); // always a string
const ageNum = Number(form.age); // convert inputs to a number
const ref = String(form.referrer ?? "").trim(); // optional code
// Guard clauses (early returns) make intent clear and avoid nested blocks
if (!username) {
console.warn("validateSignup: missing username", { username });
return { ok: false, reason: "Username is required." };
}
if (Number.isNaN(ageNum)) {
console.warn("validateSignup: age not a number", { age: form.age });
return { ok: false, reason: "Age must be a number." };
}
if (ageNum < 13) {
return { ok: false, reason: "You must be at least 13." };
}
// Optional field: use truthiness to mean "provided and non-empty"
// but then use an explicit length check because empty string is falsy
if (ref && ref.length !== 6) {
return { ok: false, reason: "Referrer code must be exactly 6 characters." };
}
return { ok: true };
}
// Reproducible calls:
console.log(validateSignup({ username: " Ada ", age: "14" })); // ok
console.log(validateSignup({ username: "", age: "16" })); // missing username
console.log(validateSignup({ username: "Ada", age: "x" })); // age NaN
console.log(validateSignup({ username: "Ada", age: 12 })); // too young
console.log(validateSignup({ username: "Ada", age: 16, referrer: "abc" })); // ref wrong lengthWhy these choices? Truthiness vs explicit comparisons
Truthiness answers “is there anything here?”—it’s cheap and readable for presence checks, especially on strings after trim(). But truthiness conflates several values (0, "", null, undefined, NaN) as falsy. Use explicit checks when 0 or empty collections are valid inputs, or when you need precise behavior.
- Truthiness good: "did the user type a username?" →
if (username). - Strict comparisons good: numeric ranges, exact lengths, or equality checks →
if (qty === 0),if (ref.length !== 6). - Read MDN for the authoritative list of falsy values: Truthy.
Truth tables for JavaScript conditionals
When you reason about combinations of booleans or boolean-like values, a compact truth table removes ambiguity. Two important notes for JavaScript: logical operators short‑circuit and they return the actual operand (not always a boolean). For conditional testing you care about truthiness of the result; for expressions you care about the returned value.
// Canonical boolean truth table (truthiness used in conditionals)
A B A && B A || B !A
true true true true false
true false false true false
false true false true true
false false false false true
// JS nuance: operators return operands, not just true/false:
null && "x" // null (falsy)
"foo" && "x" // "x" (truthy)
0 || "default"// "default"Use a truth table when you maintain complex combinations (a && (b || c)) or when multiple flags interact. It forces you to enumerate edge cases instead of assuming operator precedence will do the "right" thing.
Guard clauses: keep conditions local and readable
Return early from a function on failure. Advantages:
- Each condition states one intention. Tests are easier to read and unit-test.
- Reduces nesting, so success path stays near the top of the function.
- Failure modes are explicit: you can log the reason before returning.
// Bad: nested and hard to follow
function old(f) {
if (f) {
if (f.name) {
if (!Number.isNaN(Number(f.age))) {
// ... continue
}
}
}
}
// Better: guard clauses
function better(f) {
if (!f) return notify("missing form");
if (!f.name) return notify("missing name");
const age = Number(f.age);
if (Number.isNaN(age)) return notify("age not a number");
// success path continues here
}Advanced guard patterns: group related guards into semantic checks (e.g., const missing = missingUserFields(form)), return structured error objects ({ ok:false, code: "MISSING_USERNAME" }) for programmatic handling, and in async code reject or throw early instead of nesting callbacks. Avoid guards that mutate shared state as side effects; guards should be pure checks whenever possible so unit tests can target them directly.
Nested-condition debugging: strategies that scale
Deeply nested if/else blocks are the most time-consuming to debug. Triage with the following approach:
- Binary isolation: comment or temporarily return early at midpoints to narrow which level behaves unexpectedly.
- Instrument finely: insert concise logs that include both value and type (
console.log("x:", x, "type:", typeof x)) and include a short path label so you can see which branch executed. - Extract predicates: move complex conditions into named functions (
const isEligible = (u,a) => u && a >= 18). Small functions are easier to unit-test and reason about. - Use a truth table or small matrix (below) to exhaustively run combinations in a unit test or REPL session.
- Look for mutation across branches: shared variables changed in one branch can affect later checks. Prefer immutable names for normalized inputs.
These tactics reduce assumption-driven debugging: instead of "I think this is true", you get repeatable, labeled observations you can assert in tests.
Small test matrix (for validateSignup)
Hand‑written matrices make it easy to see coverage for common and edge cases. Below is a compact matrix you can convert to unit tests.
| Case | username | age | referrer | Expected | Notes |
|---|---|---|---|---|---|
| Valid | "Ada" | "14" | undefined | ok | Normalization and numeric parse succeed |
| Missing username | "" | "16" | - | fail | truthiness check should reject |
| Age non-number | "Ada" | "x" | - | fail | Number.isNaN guard |
| Too young | "Ada" | 12 | - | fail | numeric range check |
| Bad ref | "Ada" | 16 | "abc" | fail | truthy ref + length check |
Switch vs lookup object: decision rules and examples
Choice depends on the shape of the problem.
- Use switch when you compare a single (normalized) value to a moderate number of distinct branches and branches are short.
- Use a lookup object (or Map) when you map keys to data or small functions — it treats cases as data and reduces the risk of fallthrough and forgotten defaults.
Switch example (watch for fallthrough)
function dayLabel(input) {
const day = String(input ?? "").toLowerCase();
switch (day) {
case "mon":
case "monday":
return "Start of week"; // stacked cases intentional
case "wed":
case "wednesday":
return "Midweek";
case "fri":
case "friday":
return "Almost weekend";
default:
return "Just another day";
}
}Failure mode: a missing break or return in switch can silently fall through. Keep cases short, or prefer a lookup for many entries.
Lookup object example (preferred for mapping)
const messages = {
mon: "Start of week",
monday: "Start of week",
wednesday: "Midweek",
friday: "Almost weekend"
};
function lookupDay(input) {
const key = String(input ?? "").toLowerCase();
// Normalize and provide a safe default
return messages[key] ?? "Just another day";
}Advantages: clear data shape, easy to add or test entries, fewer control-flow bugs. If you need functions as values, use functions as the mapped values (or a Map if keys are non-string).
Targeted symptom → decision checklist (fast fixes)
- Symptom: "My if block never runs." Likely cause: value is falsy ("" / 0 / null / undefined / NaN). Decision: log the value and its type, then decide whether to use truthiness or an exact comparison (e.g.,
=== 0). - Symptom: "Else always runs." Likely cause: used loose equality or wrong variable. Decision: switch to
===, verify variable names, add console logs for both operands. - Symptom: "Condition works once then acts weird." Likely cause: assignment in condition (
=). Decision: change to comparison (===); enable a linter rule to catch this. - Symptom: "if (x === 5 || 7) is always true." Cause: second operand is a literal truthy value. Decision: repeat the comparison:
if (x === 5 || x === 7). - Symptom: "Empty array passes presence check." Cause: objects/arrays are always truthy. Decision: check size (
arr.length > 0). - Symptom: "Switch falls through." Cause: missing
breakor return. Decision: add explicitbreakor convert to a lookup. - Symptom: "Strings from inputs compare strangely." Cause: values are strings, not numbers. Decision: convert with
Number()once and guard withNumber.isNaN().
Practical debugging steps that pay off
- Log value and type immediately before the conditional:
console.log("age =", age, "typeof =", typeof age). - Normalize once at the top of a function (trim strings, convert numbers) and reuse the normalized variables.
- Parenthesize mixed
&&and||expressions so intent is explicit. - Break long conditions into named booleans:
const isAdult = age >= 18;makes tests self-documenting. - Test boundary values (0, 1, -1, empty string) and unusual types (null, undefined, NaN).
- If a branch shouldn't run, log why it didn’t:
if (!username) { console.warn("Missing username"); return; }.
Limitations and failure modes to keep in mind
- if/else chains that grow beyond ~5 branches often hide missing cases — they become hard to read and to unit-test. Consider a lookup or splitting logic into smaller functions.
- Switch statements without a
defaultcan silently ignore unexpected values introduced later. - Truthiness checks conflate several falsy values; if 0 or
""are legitimate, prefer explicit comparisons. - Mapping keys are sensitive to normalization: forget to
toLowerCase()ortrim()and the lookup will miss values. - Some bugs only appear with real user data (extra whitespace, non‑ASCII characters, numeric strings). Add small tests that mirror real inputs.
Where to go next (resources)
- MDN reference for if/else for syntax and edge cases: if...else.
- MDN glossary on truthy/falsy values: Truthy.
- Vandutz primer on variables (useful for normalization patterns): Variables: var, let, const.
- Vandutz overview of functions and why early return pairs well with small functions: Functions explained.
- Practice applying conditions to collections: Arrays explained.

Alex Carter is the editorial name behind Vandutz Academy, a programming blog for beginners. Alex reviews and tests the examples and explanations published on the site, with a focus on making Python, JavaScript, web development, and developer tools easier to understand.