JavaScript Conditionals

6 minute read

Conditionals are how your code makes decisions: if this, then that — otherwise something else. JavaScript gives you if/else if/else, the ternary operator, and short-circuit logical operators. The hard part isn’t the syntax; it’s understanding which values JavaScript counts as “truthy” or “falsy” — get that wrong and you’ll write bugs that hide for months.

if / else if / else

const score = 87;

if (score >= 90) {
  console.log("A");
} else if (score >= 80) {
  console.log("B");
} else if (score >= 70) {
  console.log("C");
} else {
  console.log("F");
}
// Output: B

Always use braces, even for single statements. The “saved keystrokes” form is a known footgun:

// ❌ Looks fine, but adding a line later breaks it
if (cond)
  doA();
  doB();   // always runs — not part of the if!

// ✅ Always braces
if (cond) {
  doA();
  doB();
}

Truthy and Falsy

The condition expression is coerced to boolean. JavaScript has exactly eight falsy values; everything else is truthy.

Falsy Truthy
false true
0, -0, 0n any non-zero number
"" (empty string) any non-empty string, including "0" and "false"
null [] (empty array!)
undefined {} (empty object!)
NaN functions, dates, etc.
if ([]) console.log("yes");           // yes  ← empty array is truthy
if ({}) console.log("yes");           // yes
if ("0") console.log("yes");          // yes  ← non-empty string
if ("false") console.log("yes");      // yes
if (0) console.log("yes");            // (no)
if (NaN) console.log("yes");          // (no)

// Quick boolean coercion
Boolean([]);     // true
!!"";            // false
!!"hello";       // true

Warning: Empty arrays and objects are truthy. To check “is this collection empty?”, check arr.length === 0 or Object.keys(obj).length === 0.

Comparison Operators

Operator Compares Coerces?
=== strict equality ❌ no
!== strict inequality ❌ no
== loose equality ✅ yes
!= loose inequality ✅ yes
<, >, <=, >= order ✅ yes
1 === 1;            // true
1 === "1";          // false  ← strict, no coercion
1 == "1";           // true   ← loose, coerces
0 == false;         // true   ← coerces to 0 == 0
null == undefined;  // true   ← special-case
null === undefined; // false
NaN === NaN;        // false  ← always

Tip: Use === and !== exclusively. The two exceptions where loose == is acceptable: comparing against null to also catch undefined (x == null). Otherwise, never.

// Idiomatic null+undefined check
if (value == null) {
  // value is null OR undefined
}

// Or use ?? if you want a fallback
const name = input ?? "guest";

Logical Operators — and Short-Circuit

true  && false;    // false
true  || false;    // true
!true;             // false

&& and || don’t return booleans — they return one of the operands:

"a" && "b";        // "b"     — both truthy → returns last
0   && "b";        // 0       — first falsy → returns it
"a" || "b";        // "a"     — first truthy
""  || "fallback"; // "fallback"  — first truthy
null || 0 || "x";  // "x"     — keeps walking until truthy

Three patterns you’ll see daily:

// Default value (legacy)
const name = userName || "guest";   // ⚠ replaces "" or 0 too!

// Default value (modern, ES2020) — only on null/undefined
const name = userName ?? "guest";

// Short-circuit "if x then run y"
isLoggedIn && showDashboard();

// Optional chaining
user?.profile?.bio;
Operator Falls through on Use when
\|\| falsy (incl. 0, "", false) “any falsy → fallback”
?? only null / undefined “missing → fallback”

Warning: userInput || defaultValue overrides legitimate 0 or "". If those are valid values, use ?? — that’s its whole reason for existing.

Ternary Operator

A compact if/else that returns a value:

const status = age >= 18 ? "adult" : "minor";

// Nested (use sparingly)
const grade =
  score >= 90 ? "A" :
  score >= 80 ? "B" :
  score >= 70 ? "C" : "F";

Use ternary when:

  • ✅ Both branches produce a value (not side effects)
  • ✅ The whole expression fits clearly on one or two lines

Don’t use ternary when:

  • ❌ Branches do side effects (a > b ? doThis() : doThat()) — use if
  • ❌ It nests three deep — use if chain or a lookup table

Lookup Tables — When You Have Many Branches

If your if/else if chain is just mapping one value to another, use an object:

// ❌ verbose
function grade(score) {
  if (score >= 90) return "A";
  else if (score >= 80) return "B";
  else if (score >= 70) return "C";
  else return "F";
}

// ✅ data-driven
const GRADE_MAP = [
  [90, "A"],
  [80, "B"],
  [70, "C"],
  [0,  "F"],
];

function grade(score) {
  return GRADE_MAP.find(([min]) => score >= min)?.[1];
}

// ✅ flat key map
const ROLE_LABEL = {
  admin: "Administrator",
  user:  "Standard User",
  guest: "Guest",
};

const label = ROLE_LABEL[role] ?? "Unknown";

Guard Clauses

Avoid deep nesting by returning early when a precondition fails:

// ❌ Nested
function process(input) {
  if (input != null) {
    if (input.length > 0) {
      if (input.valid) {
        // …actual work, 3 levels deep
      }
    }
  }
}

// ✅ Guard clauses
function process(input) {
  if (input == null)       return;
  if (input.length === 0)  return;
  if (!input.valid)        return;

  // actual work, 0 levels deep
}

A Realistic Example — Form Field Validator

function validateEmail(value) {
  if (value == null || value === "") {
    return { ok: false, error: "Email is required" };
  }

  const trimmed = value.trim();
  if (trimmed.length > 254) {
    return { ok: false, error: "Email too long" };
  }

  const looksLikeEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(trimmed);
  if (!looksLikeEmail) {
    return { ok: false, error: "Invalid email format" };
  }

  return { ok: true, value: trimmed };
}

validateEmail("");               // { ok: false, error: "Email is required" }
validateEmail("not-an-email");   // { ok: false, error: "Invalid email format" }
validateEmail(" ada@x.io ");     // { ok: true, value: "ada@x.io" }

Guard clauses, == for null/undefined, regex for shape, trimmed value returned. Each branch is short and the happy path is at the bottom.

Common Mistakes

// ❌ Truthy-check on a number that could be 0
if (count) renderCount(count);   // skips zero!

// ✅ Be explicit
if (count != null) renderCount(count);
if (count !== 0) renderCount(count);

// ❌ Loose equality with strings/numbers
"10" == 10;        // true   ← surprising
"" == false;       // true
[] == false;       // true
[1] == 1;          // true

// ✅ Always === / !==

// ❌ Returning undefined from a ternary side effect
const x = condition ? foo() : null;
// foo() returns undefined → x is undefined, easy to miss

// ❌ Negated condition for the "common" path
if (!user.isAdmin) {
  /* short branch */
} else {
  /* long branch */
}
// Flip it — put the common branch first, no negation

Best Practices Checklist

  • ✅ Use === / !== always (except x == null for null+undefined)
  • ✅ Use guard clauses to keep code flat
  • ✅ Use ?? for null/undefined defaults; || only when you genuinely want any falsy to fall through
  • ✅ Use lookup objects/arrays instead of long if/else chains
  • ✅ Always brace the body of if/else
  • ❌ Don’t use if (value) to mean “is set” if 0/""/false are valid
  • ❌ Don’t nest ternaries past one level
  • ❌ Don’t use side-effect ternaries — use if

What’s Next?

if/else covers most cases. When you have a value to dispatch on with many branches, JavaScript’s switch is occasionally cleaner — but it has its own quirks worth knowing.

← Previous: JS Math Object Next: JS Switch →

Updated: