if, switch and the conditional operator

Beginner35 min

Branch in idiomatic JavaScript with if/else, guard clauses, the conditional operator, switch and short-circuit operators.

What you will learn
  • Write if, else if and else chains and use truthiness in conditions
  • Flatten nested logic with guard clauses
  • Use the conditional operator for choosing a value
  • Write a switch statement and avoid accidental fall-through
  • Use && and || as expressions that return one of their operands
On this page

Choosing what to run next is the core of control flow. The idea is explained in the fundamentals lesson on conditionals. This lesson shows how JavaScript writes it, and the few spots where it differs from what you may expect.

if, else if and else

The condition goes in brackets, and the branch is a statement or a block. Always use a block, even for one line, so that adding a second line later does not silently fall outside the if.

if_else.mjs
const celsius = 20;
if (celsius < 0) {
console.log("freezing");
} else if (celsius < 15) {
console.log("cold");
} else if (celsius < 25) {
console.log("mild");
} else {
console.log("hot");
}
Output
mild

The chain is tested from the top, and the first true condition wins. Change celsius to -5, 10 or 31 and run it again to see the other branches.

A condition does not have to be a boolean. JavaScript converts it using the truthy and falsy rules from the coercion lesson: false, 0, "", null, undefined, NaN and 0n are falsy, and everything else is truthy.

truthy_if.mjs
const name = "";
if (name) {
console.log("has a name");
} else {
console.log("name is empty");
}
Output
name is empty

That is why if (name) is false for an empty string. It is also why if (count) is false when count is 0, which is a classic bug when 0 is a valid value. Write the comparison you mean: if (count !== undefined) or if (count > 0).

Check the special cases first

Deeply nested if statements are hard to read. Put the special or bad case first, so each later branch only deals with what is left.

shipping_checks.mjs
const itemCount = 2;
const total = 60;
if (itemCount === 0) {
console.log("nothing to ship");
} else if (total >= 50) {
console.log("free shipping");
} else {
console.log("shipping: 4.99");
}
Output
free shipping

Read top to bottom, each test removes one case from consideration. Inside a function you can leave early with return instead of using else. That style is called a guard clause, and the function forms lesson comes back to it.

The conditional operator

The conditional operator condition ? a : b is an expression. It evaluates to a when the condition is truthy and to b otherwise. Use it to choose a value, where an if statement would not fit.

ternary.mjs
const age = 20;
const label = age >= 18 ? "adult" : "minor";
console.log(label);
const n = 0;
console.log(`${n} item${n === 1 ? "" : "s"}`);
const score = 72;
const grade = score >= 90 ? "A" : score >= 80 ? "B" : score >= 70 ? "C" : "F";
console.log(grade);
Output
adult
0 items
C

It works well inside template literals, as in the plural example. Chaining it, as the grade line does, is legal and reads acceptably for a short table. Once a chain needs more than two or three tests, switch to if statements. Do not use ? : for side effects, such as ok ? save() : warn(). An if says that more clearly.

switch

switch compares one value against several case labels. It is useful when many branches test the same value for equality.

switch_basic.mjs
const day = "sun";
switch (day) {
case "sat":
case "sun":
console.log("weekend");
break;
case "mon":
case "tue":
case "wed":
case "thu":
case "fri":
console.log("weekday");
break;
default:
console.log("unknown");
}
Output
weekend

The break ends the switch once a case has run. Two labels in a row, such as "sat" and "sun", share the code below them. default runs when nothing else matches. It may go anywhere, but last is the convention.

Cases fall through: execution continues into the next case until it meets break, return or the end of the switch.

switch_fallthrough.mjs
const level = 2;
switch (level) {
case 1:
console.log("one");
case 2:
console.log("two");
case 3:
console.log("three");
break;
case 4:
console.log("four");
}
Output
two
three

Here level is 2, so it prints two and also three, because case 2 has no break. Falling through on purpose is how "sat" and "sun" share a result. Falling through by accident, from a missing break, is a frequent bug.

Matching uses strict equality (===), so a string does not match a number:

switch_strict.mjs
switch ("5") {
case 5:
console.log("matched the number");
break;
default:
console.log("strict comparison: no match");
}
Output
strict comparison: no match

All the cases share one block scope. If you need a let or const inside a case, give the case its own braces.

switch_scope.mjs
const shape = "circle";
const size = 2;
switch (shape) {
case "square": {
const side = size;
console.log(side * side);
break;
}
case "circle": {
const radius = size;
console.log(Math.round(Math.PI * radius ** 2));
break;
}
default:
console.log(`unknown shape: ${shape}`);
}
Output
13

Without the braces, two cases that each declared const side would clash in one scope and be a SyntaxError.

When the branches test different conditions, or use ranges, an if chain is the better fit. Many switch statements are better replaced by an object that maps the value to a result, which the objects lesson touches on.

Short-circuit operators

In JavaScript && and || do not return true or false. They return one of their operands.

  • a && b returns a if a is falsy, otherwise b.
  • a || b returns a if a is truthy, otherwise b.

They also short-circuit: the right side is not evaluated when the left side already decides the result.

short_circuit.mjs
const user = { name: "Ada", admin: false };
console.log(user.name && user.name.toUpperCase());
console.log("" && "never reached");
console.log(user.nickname || user.name);
console.log(0 || "" || null);
console.log(1 && 2 && 3);
user.admin && console.log("admin tools");
user.admin || console.log("not an admin");
Output
ADA
Ada
null
3
not an admin

user is an object, a record with named parts, which the objects lesson covers. The first line is the guard idiom: only call toUpperCase when user.name is there. The second prints an empty line, which is the empty string that && returned. || returns the first truthy operand, or the last operand when none is truthy: 0 || "" || null is null.

The last two lines use short-circuiting as a statement. It works, but an if is clearer, and linters often flag it. Use && and || for values, and if for actions. For defaults, remember from the lesson on null, undefined and nullish operators that ?? is usually safer than ||.

Operator precedence

When an expression has several operators, precedence decides which one is worked out first, and associativity decides the order between operators of the same level. This table runs from the operators that bind most tightly to the loosest:

Level Operators
Grouping ( )
Member access and calls a.b, a[0], f()
Unary !, -x, +x, typeof, ++, --
Power ** (works right to left)
Multiply *, /, %
Add +, -
Comparison <, <=, >, >=, instanceof, in
Equality ===, !==, ==, !=
And &&
Or, nullish ||, ??
Conditional ? :
Assignment =, +=, ??= and the other compound forms

Five cases cause most of the surprises:

precedence.mjs
console.log(2 + 3 * 4, (2 + 3) * 4);
console.log(true || false && false, (true || false) && false);
console.log(!true == false, !(true == false));
console.log(2 ** 3 ** 2, (2 ** 3) ** 2);
console.log(10 - 4 - 3, 10 - (4 - 3));
Output
14 20
true false
true true
512 64
3 9
  • * before +, as in school arithmetic: 14 against 20.
  • && binds tighter than ||, so true || false && false is true || (false && false). Brackets give the other reading.
  • ! binds tighter than ==, so !true == false is (!true) == false, which is true. The comparison is not negated.
  • ** works right to left: 2 ** 3 ** 2 is 2 ** 9. Subtraction works left to right, so 10 - 4 - 3 is (10 - 4) - 3.
  • ?? cannot be mixed with || or && without brackets, as the nullish lesson showed. A minus sign in front of a ** base, as in -2 ** 2, is a SyntaxError.

When in doubt, add brackets. They cost nothing and tell the reader what you meant. instanceof and in are comparison operators that test where an object came from and whether it has a property. They come up in later lessons.

Common pitfalls

  • A missing break. Each case falls into the next one. Add break or return, or leave a comment where fall-through is deliberate.
  • Assignment instead of comparison. if (x = 5) assigns and is always truthy. Use ===.
  • Testing truthiness when 0 or "" are valid. Compare explicitly.
  • Nested ? : chains that nobody can read. Switch to if or a lookup.
  • Expecting switch to match across types. It uses ===, so "5" and 5 are different cases.
  • Relying on precedence you have not checked. Mixed && and || deserve brackets.

Exercises

ExerciseLeap year checker

A year is a leap year if it is divisible by 4, except years divisible by 100, unless they are also divisible by 400. Declare const year = 2000;, work out whether it is a leap year with one boolean expression, and print 2000 is a leap year or 2000 is not a leap year. Then try 1900, 2023 and 2024.

Input: none. Expected output for 2000: 2000 is a leap year. For 1900: 1900 is not a leap year. For 2023: 2023 is not a leap year. For 2024: 2024 is a leap year.

Show solution
leap_year.mjs
const year = 2000;
const isLeap = (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
console.log(`${year} is ${isLeap ? "a leap year" : "not a leap year"}`);
Output
2000 is a leap year

The brackets group “divisible by 4 and not by 100”, and || adds the exception for 400. Without the brackets the same result would follow from the precedence table, but the brackets say it out loud.

ExerciseGrade calculator

Declare const score = 79.5;. Print A for 90 and above, B for 80, C for 70, D for 60 and F below that. Anything that is not a number from 0 to 100 prints invalid. Try the scores 95, 80, 60, 12, 101, "90", NaN.

Input: none. Expected output for 79.5: C. For the others: A, B, D, F, invalid, invalid, invalid.

Show solution
grade.mjs
const score = 79.5;
const notANumber = typeof score !== "number" || Number.isNaN(score);
if (notANumber || score < 0 || score > 100) {
console.log("invalid");
} else if (score >= 90) {
console.log("A");
} else if (score >= 80) {
console.log("B");
} else if (score >= 70) {
console.log("C");
} else if (score >= 60) {
console.log("D");
} else {
console.log("F");
}
Output
C

The first branch rejects everything that is not a usable score. NaN has the type "number", so it needs its own check. Then each later branch only needs a lower bound, because higher scores were handled above. The string "90" is rejected on purpose: converting input to a number is a separate step.

ExercisePredict the precedence

Work out the six lines this program prints, then run it.

console.log(2 + 3 * 2 ** 2);
console.log(10 - 4 - 3);
console.log(true || false && false);
console.log(!false == true);
console.log(1 + 1 === 2 && 3 > 2 || false);
console.log(2 ** 2 ** 3);

Input: none. Expected output: 14, 3, true, true, true, 256.

Show solution
predict_precedence.mjs
console.log(2 + 3 * 2 ** 2);
console.log(10 - 4 - 3);
console.log(true || false && false);
console.log(!false == true);
console.log(1 + 1 === 2 && 3 > 2 || false);
console.log(2 ** 2 ** 3);
Output
14
3
true
true
true
256

First 2 ** 2 is 4, then 3 * 4 is 12, then 2 + 12 is 14. Subtraction goes left to right: (10 - 4) - 3. && goes before ||. !false is true, and true == true. In the fifth line 1 + 1 === 2 and 3 > 2 are both true, so true && true || false is true. The last line is 2 ** (2 ** 3), which is 2 ** 8.

Summary

  • Use if, else if and else with blocks. Conditions use truthiness, so compare explicitly when 0 or "" are valid values.
  • Put the special cases first and keep each branch short.
  • condition ? a : b chooses a value. Keep it short.
  • switch uses ===, falls through until break or return, and shares one scope between cases.
  • && and || return an operand, not a boolean, and stop early.
  • Precedence decides the order of operators. && goes before ||, ** goes right to left, and brackets remove all doubt.

Next: repeating work with loops, in the loops lesson.