Conditionals

Mental model: A statement picks a path; an expression produces a value. Choose the one that matches what you actually need.

Level: beginner · about 10 minutes

const score = 82;

if (score >= 90) {
  console.log('A');
} else if (score >= 80) {
  console.log('B');   // → 'B'
} else {
  console.log('C or below');
}

Run this, then change the score to 55 and predict the output before running again.

An if statement runs a block when its condition is truthy. else if chains are checked top to bottom and stop at the first match, which is why the order of your branches is part of the logic, not a style choice.

The condition is coerced to a boolean, so if (list.length) and if (name) work. That is convenient and occasionally a trap: 0, '', NaN, null and undefined are all falsy, so if (count) skips a real count of zero.

function describe(count) {
  if (count) return 'has items';        // 0 falls through
  return 'empty';
}

console.log(describe(3));   // → 'has items'
console.log(describe(0));   // → 'empty'  ... a real zero, treated as missing

// Say what you mean instead:
const better = (count) => (count > 0 ? 'has items' : 'empty');
console.log(better(0));     // → 'empty', and now it is deliberate

The ternary is an expression

An if statement cannot be assigned to anything. A ternary can, because it evaluates to a value. That single difference decides which one to reach for.

Statement: assign in both branches

let label;
if (isAdmin) {
  label = 'Admin';
} else {
  label = 'Member';
}

Expression: one const

const label = isAdmin
  ? 'Admin'
  : 'Member';

The ternary version needs no let and no reassignment, so a reader can see at a glance that label never changes again. Use a ternary when you want a value, and an if when you want an action.

Early return beats nesting

Nested: read down and right

function pay(order) {
  if (order) {
    if (order.items.length) {
      if (order.paid) {
        return 'ship it';
      }
    }
  }
  return 'wait';
}

Early return: read down only

function pay(order) {
  if (!order) return 'wait';
  if (!order.items.length) return 'wait';
  if (!order.paid) return 'wait';
  return 'ship it';
}

Handle the reasons to stop first, one per line, then write the happy path at the end with zero indentation. Each guard is now a sentence you can read on its own.

switch compares with ===

function planFor(role) {
  switch (role) {
    case 'owner':
    case 'admin':          // deliberate fallthrough, both get the same body
      return 'full access';
    case 'member':
      return 'read and write';
    default:
      return 'read only';
  }
}

console.log(planFor('admin'), planFor('member'), planFor('ghost'));

Note the deliberate grouping: two labels share one body.

const id = '2';
switch (id) {
  case 2:
    console.log('number two');
    break;
  default:
    console.log('no match');
}

switch uses strict equality, so '2' === 2 is false and the only branch left is default. This is the single most common switch bug, and it usually arrives via input.value, which is always a string. Convert first with Number(id).

When the switch is really a table

A long switch where every branch just returns a value is a lookup table written the slow way. Replace it with an object or a Map and the data becomes something you can iterate, count and test.

const LABELS = new Map([
  [200, 'OK'],
  [301, 'Moved Permanently'],
  [404, 'Not Found'],
  [500, 'Server Error'],
]);

const labelFor = (code) => LABELS.get(code) ?? 'Unknown status';

console.log(labelFor(404));        // → 'Not Found'
console.log(labelFor(418));        // → 'Unknown status'
console.log(labelFor('toString')); // → 'Unknown status'  (a plain object would leak here)
You wantReach forBecause
To do something in one caseifno value is produced
To choose between two valuesternaryit is an expression, so const works
To reject bad inputearly return guardsflat, one reason per line
Three or more branches on one valueswitch or a lookupthe value is compared once
A branch per key, returning dataobject or Map lookupdata beats code you have to read

Try it yourself

Flatten the nesting

function checkout(order) {
  if (order) {
    if (order.items && order.items.length > 0) {
      if (order.paid) {
        return 'ship';
      } else {
        return 'awaiting payment';
      }
    } else {
      return 'empty cart';
    }
  } else {
    return 'no order';
  }
}

console.log(checkout(null));
console.log(checkout({ items: [] }));
console.log(checkout({ items: ['book'], paid: false }));
console.log(checkout({ items: ['book'], paid: true }));

Rewrite checkout using early returns so nothing is indented more than one level. Then add a rule: an order over 1000 needs order.approved.

Switch versus lookup

function symbolSwitch(currency) {
  switch (currency) {
    case 'USD': return '$';
    case 'EUR': return '€';
    case 'GBP': return '£';
    case 'JPY': return '¥';
    default: return '?';
  }
}

const SYMBOLS = { USD: '$', EUR: '€', GBP: '£', JPY: '¥' };
const symbolLookup = (currency) =>
  Object.hasOwn(SYMBOLS, currency) ? SYMBOLS[currency] : '?';

console.log(symbolSwitch('EUR'), symbolLookup('EUR'));
console.log(symbolSwitch('XYZ'), symbolLookup('XYZ'));
console.log(Object.keys(SYMBOLS).length, 'currencies, countable without reading any code');

Add a fifth currency to both versions and notice which edit you would rather make on a Friday afternoon.

Exercises

FizzBuzz, but as a function

Write fizzBuzz(n). Return 'Fizz' when n divides by 3, 'Buzz' when it divides by 5, 'FizzBuzz' when it divides by both, and otherwise the number as a string. Order your branches so the both case cannot be swallowed.

Replace the switch with a lookup

Rewrite httpLabel(code) without a switch. Use a Map of numeric codes to labels and return 'Unknown status' for anything missing. Keys are numbers, so the string '200' must not match, and neither must 'toString'.

Check yourself

What does this log?
'high' — The chain stops at the first truthy condition, and 500 >= 10 matches immediately, so 'very high' is unreachable. Overlapping branches must go from narrowest to widest.
Why can you write const label = a ? b : c but not const label = if (a) b else c?
A ternary is an expression and produces a value; if is a statement and does not — Expressions evaluate to a value you can assign, pass or return. Statements perform an action. That is why a ternary fits inside a template literal or a JSX attribute and an if does not.
You have twelve branches that each return a different string for a known key. What is usually the best shape?
An object or Map lookup with a fallback — Branches that only map a key to a value are data. Stored as a lookup they can be counted, iterated, validated and tested, and adding the thirteenth entry is a one-line edit rather than a code change.

Common mistakes

  • Using if (count) when zero is a legitimate value. Compare explicitly with count > 0.
  • Ordering else if branches from widest to narrowest, so the specific case never runs.
  • Forgetting that switch uses ===, then wondering why a numeric case never matches a value read from an input.

Takeaways

  • else if chains stop at the first match, so branch order is part of the behaviour.
  • A ternary produces a value; an if performs an action. Pick by what you need.
  • Guard clauses turn nesting into a flat list of reasons to stop early.
  • switch compares strictly, and a returning-only switch is usually a lookup in disguise.