if, switch and the conditional operator
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.
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");}mildThe 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.
const name = "";
if (name) { console.log("has a name");} else { console.log("name is empty");}name is emptyThat 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.
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");}free shippingRead 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.
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);adult0 itemsCIt 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.
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");}weekendThe 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.
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");}twothreeHere 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 ("5") { case 5: console.log("matched the number"); break; default: console.log("strict comparison: no match");}strict comparison: no matchAll the cases share one block scope. If you need a let or const inside a case, give the case its own braces.
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}`);}13Without 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 && breturnsaifais falsy, otherwiseb.a || breturnsaifais truthy, otherwiseb.
They also short-circuit: the right side is not evaluated when the left side already decides the result.
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");ADA
Adanull3not an adminuser 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:
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));14 20true falsetrue true512 643 9*before+, as in school arithmetic: 14 against 20.&&binds tighter than||, sotrue || false && falseistrue || (false && false). Brackets give the other reading.!binds tighter than==, so!true == falseis(!true) == false, which istrue. The comparison is not negated.**works right to left:2 ** 3 ** 2is2 ** 9. Subtraction works left to right, so10 - 4 - 3is(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 aSyntaxError.
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. Eachcasefalls into the next one. Addbreakorreturn, 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
0or""are valid. Compare explicitly. - Nested
? :chains that nobody can read. Switch toifor a lookup. - Expecting
switchto match across types. It uses===, so"5"and5are different cases. - Relying on precedence you have not checked. Mixed
&&and||deserve brackets.
Exercises
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
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"}`);2000 is a leap yearThe 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.
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
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");}CThe 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.
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
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);143truetruetrue256First 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 ifandelsewith blocks. Conditions use truthiness, so compare explicitly when0or""are valid values. - Put the special cases first and keep each branch short.
condition ? a : bchooses a value. Keep it short.switchuses===, falls through untilbreakorreturn, 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.