Control flow Foundation
Making decisions
Let your program choose what to do with if, else if and else, the ternary operator and switch, and keep decisions readable with guard clauses.
BY THE END OF THIS LESSON YOU CAN
- Choose between branches with if, else if and else, and order the conditions correctly
- Pick between two values with the ternary operator
- Compare one value against fixed options with switch, and avoid accidental fall-through
- Flatten nested conditions into guard clauses that fail closed
- Validate outside input before deciding, and test the boundary values
if and else
So far every line of your programs ran, top to bottom. Real programs have to choose: save the task, or reject it because the title is empty. An if statement runs a block of code only when a condition is truthy:
const title = "";
if (title.trim() === "") {
console.log("A task needs a title");
}
console.log("This line always runs");
node if.js and of the browser terminalA task needs a title This line always runs
The condition goes in parentheses. The code to run goes in a block, between { and }. Indent the code inside a block by two spaces so you can see at a glance what belongs to the if.
Add else to say what happens when the condition is falsy. Exactly one of the two blocks runs:
const title = "Buy milk";
if (title.trim() === "") {
console.log("A task needs a title");
} else {
console.log(`Saving "${title}"`);
}
node if-else.js and of the browser terminalSaving "Buy milk"
The condition does not have to be a boolean. JavaScript converts it using the truthy and falsy rules from Values, variables and types. if (title) is true for any non-empty string. That is handy, but be careful with numbers: if (count) is false when count is 0.
Several choices with else if
When there are more than two possibilities, chain them with else if. JavaScript tests the conditions from top to bottom and runs only the first block whose condition is truthy. else at the end catches everything else.
const hoursLeft = 5;
if (hoursLeft < 0) {
console.log("Overdue");
} else if (hoursLeft < 24) {
console.log("Due today");
} else if (hoursLeft < 24 * 7) {
console.log("Due this week");
} else {
console.log("Due later");
}
node else-if.js and of the browser terminalDue today
5 is also less than 24 * 7, but "Due this week" did not print: once one branch runs, the others are skipped. So the order matters. Put the most specific conditions first.
Nested conditions
A block can contain another if. This is called nesting:
const loggedIn = true;
const role = "viewer";
if (loggedIn) {
if (role === "admin") {
console.log("Show the admin panel");
} else {
console.log("Show the normal dashboard");
}
} else {
console.log("Show the login page");
}
node nested.js and of the browser terminalShow the normal dashboard
One level of nesting is fine. Three or four levels become hard to follow. The section on guard clauses below shows how to flatten them.
The ternary operator
Sometimes you only want to choose between two values. The ternary operator condition ? a : b gives a when the condition is truthy and b otherwise. It is called "ternary" because it takes three parts.
const done = true;
const count = 1;
const mark = done ? "x" : " ";
console.log(`[${mark}] Buy milk`);
console.log(`${count} ${count === 1 ? "task" : "tasks"} left`);
node ternary.js and of the browser terminal[x] Buy milk 1 task left
Use a ternary for a short choice between two values. For anything longer, or for choices that do things (print, save, delete), use if. Never nest ternaries inside ternaries; they quickly become unreadable.
switch
When you compare one value against many fixed options, switch can be clearer than a long else if chain. It compares with ===:
const status = "in_progress";
switch (status) {
case "todo":
console.log("Not started");
break;
case "in_progress":
case "review":
console.log("Someone is working on it");
break;
case "done":
console.log("Finished");
break;
default:
console.log(`Unknown status: ${status}`);
}
node switch.js and of the browser terminalSomeone is working on it
- Each
caseis one possible value.defaultruns when no case matches, likeelse. breakleaves theswitch. Two cases with nothing between them, like"in_progress"and"review", share the same code.
Forgetting break
Withoutbreak, JavaScript does not stop at the end of a case. It "falls through" and keeps running the code of the next cases too:const level = "warn";
switch (level) {
case "error":
console.log("page the on-call engineer");
case "warn":
console.log("write to the error log");
case "info":
console.log("write to the normal log");
}
node fallthrough.js and of the browser terminalwrite to the error log write to the normal log
Here the fall through is deliberate: a warning is also written to the normal log. Most of the time it is a bug. If you let cases fall through on purpose, add a comment saying so.
Guard clauses
Decisions usually live inside a function: a named piece of code that takes inputs and gives back a result with return. Functions get their own lesson; for now you only need this shape:
function describe(count) {
return `${count} tasks`;
}
console.log(describe(3));
node function-shape.js and of the browser terminal3 tasks
count is a parameter: a variable that gets the value you pass in the parentheses when you call the function. return ends the function and hands back a value.
Because return ends the function straight away, you can check the bad cases first and leave early. Each early check is called a guard clause. You wrote guard clauses as negated conditions in Conditional reasoning; here they are in real JavaScript. Compare the nested version with the guarded one:
function canEditNested(user, task) {
if (user) {
if (task.ownerId === user.id) {
if (!task.locked) {
return "yes";
} else {
return "no: task is locked";
}
} else {
return "no: not your task";
}
} else {
return "no: not logged in";
}
}
function canEdit(user, task) {
if (!user) return "no: not logged in";
if (task.ownerId !== user.id) return "no: not your task";
if (task.locked) return "no: task is locked";
return "yes";
}
const ada = { id: 1 };
const task = { ownerId: 1, locked: false };
console.log(canEditNested(ada, task), "|", canEdit(ada, task));
console.log(canEditNested(null, task), "|", canEdit(null, task));
console.log(canEdit({ id: 2 }, task));
console.log(canEdit(ada, { ownerId: 1, locked: true }));
node guard.js and of the browser terminalyes | yes no: not logged in | no: not logged in no: not your task no: task is locked
Both functions give the same answers, but the second one reads like a checklist: every rule is one line, and the happy path is the last line. When a block is a single statement you may leave out the braces, as in if (!user) return …;. Keep that for one-line guards.
TIP
Notice thatcanEdit starts from "no" and returns "yes" only after every check passed. For anything about permissions, always fail closed: when in doubt, refuse.Build: age verification
A sign-up form asks for the user's age. The value arrives as text, typed by a stranger, so it can be anything. The rules:
- The age must be present, a whole number, and between 0 and 130. Otherwise, reject the input with a clear reason.
- Under 13: the user cannot sign up.
- 13 to 17: they can sign up with a parent's consent.
- 18 or older: they can sign up.
REASON IT OUT
Before you code: what can the age field contain?
The age arrives from a form, typed by a stranger. Before writing checkAge, work out:
- Which inputs are not an age at all? Think of empty fields, missing fields, words, decimals and negative numbers.
- Which exact ages sit on a boundary between two answers?
- In which order must the checks run, and why can the "too young" check not come first?
Show the reasoning
Not an age: "" and " " (the field was left empty), null or undefined (the field was missing), "abc", "17.5", "-4" and "200". Each needs a clear "invalid" answer, not a decision.
Boundaries: 12 and 13 (too young or consent), 17 and 18 (consent or allowed), and 0 and 130 at the edges of the valid range. A < written where <= was meant only shows up on exactly these values, so each of them gets a test.
Order: validate first, decide second. If the "under 13" check ran first, Number(""), which is 0, would make an empty form say "too young", and Number("abc"), which is NaN, would fail every comparison and fall through to "allowed". Guard clauses that reject bad input must come before any rule that trusts the number.
First validate the input with guard clauses, then decide. Test the function with many inputs, including bad ones, and print each result:
function checkAge(input) {
if (input === undefined || input === null) return "invalid: age is required";
const text = String(input).trim();
if (text === "") return "invalid: age is required";
const age = Number(text);
if (!Number.isInteger(age)) return "invalid: age must be a whole number";
if (age < 0 || age > 130) return "invalid: age must be between 0 and 130";
if (age < 13) return "rejected: too young";
if (age < 18) return "allowed with parental consent";
return "allowed";
}
console.log("25 ->", checkAge("25"));
console.log("18 ->", checkAge("18"));
console.log("17 ->", checkAge("17"));
console.log("13 ->", checkAge(" 13 "));
console.log("12 ->", checkAge("12"));
console.log("'' ->", checkAge(""));
console.log("null ->", checkAge(null));
console.log("abc ->", checkAge("abc"));
console.log("17.5 ->", checkAge("17.5"));
console.log("-4 ->", checkAge("-4"));
console.log("200 ->", checkAge(200));
node age-check.js and of the browser terminal25 -> allowed 18 -> allowed 17 -> allowed with parental consent 13 -> allowed with parental consent 12 -> rejected: too young '' -> invalid: age is required null -> invalid: age is required abc -> invalid: age must be a whole number 17.5 -> invalid: age must be a whole number -4 -> invalid: age must be between 0 and 130 200 -> invalid: age must be between 0 and 130
Walk through a few lines of the output:
"abc"becomesNaN, andNumber.isInteger(NaN)isfalse, so one check rejects both text and decimals.- The empty string needs its own check, because
Number("")is0, as you saw in Values, variables and types. Without that guard, an empty form would say "rejected: too young". - The boundary values
18,17,13and12are tested on purpose. Bugs love the edges:<where you meant<=. - The decision part is only three lines, because the guards already threw out every bad input.
NOTE
A real sign-up form should not trust an age typed by the user for anything that matters legally. The point here is the shape: validate every outside input first, then decide.Practice
TRY IT YOURSELF
Grade letters
Write a function grade(score) that returns "A" for 90 and above, "B" for 80 to 89, "C" for 70 to 79 and "F" below 70. Test it with 95, 90, 89, 70 and 12.
Write it in the editor, then press Check. Hints and the solution open up once you have checked your code.
HINT 1
Order the checks from the highest band to the lowest, each with an early return. By the time a later check runs, you already know the score missed every check above it.
HINT 2
if (score >= 90) return "A"; if (score >= 80) return "B"; if (score >= 70) return "C"; return "F";
SOLUTION
function grade(score) {
if (score >= 90) return "A";
if (score >= 80) return "B";
if (score >= 70) return "C";
return "F";
}
console.log(grade(95), grade(90), grade(89), grade(70), grade(12));
node grade.js and of the browser terminalA A B C F
Because each guard returns, the later checks do not need an upper limit: by the time score >= 80 runs, you already know it is below 90.
TRY IT YOURSELF
HTTP status names
Write statusText(code) with a switch that returns "OK" for 200, "Created" for 201, "Not Found" for 404 and "Unknown" for anything else.
Write it in the editor, then press Check. Hints and the solution open up once you have checked your code.
HINT 1
Inside a function, return leaves the switch too, so you do not need break. Add one case per status code, and a default for the rest.
HINT 2
switch (code) { case 200: return "OK"; case 201: return "Created"; case 404: return "Not Found"; default: return "Unknown"; }
SOLUTION
function statusText(code) {
switch (code) {
case 200:
return "OK";
case 201:
return "Created";
case 404:
return "Not Found";
default:
return "Unknown";
}
}
console.log(statusText(200), "|", statusText(404), "|", statusText("200"));
node status-text.js and of the browser terminalOK | Not Found | Unknown
Inside a function, return also leaves the switch, so no break is needed. "200" is a string, and switch uses ===, so it does not match 200.
TRY IT YOURSELF
Shipping cost
Write shipping(total, country): orders of 50 or more ship free; otherwise shipping is 5 inside "NG" and 15 elsewhere. Reject a total that is not a positive number with "invalid total". Use guard clauses.
Write it in the editor, then press Check. Hints and the solution open up once you have checked your code.
HINT 1
Guard first: typeof total !== "number" || !(total > 0) catches strings, negatives, zero and NaN in one check.
HINT 2
After the guard, if (total >= 50) return 0; then return country === "NG" ? 5 : 15;.
SOLUTION
function shipping(total, country) {
if (typeof total !== "number" || !(total > 0)) return "invalid total";
if (total >= 50) return 0;
return country === "NG" ? 5 : 15;
}
console.log(shipping(80, "GB"), shipping(20, "NG"), shipping(20, "GB"));
console.log(shipping(-5, "NG"), shipping(NaN, "NG"), shipping("80", "NG"));
node shipping.js and of the browser terminal0 5 15 invalid total invalid total invalid total
!(total > 0) is true for negative numbers, zero and NaN, because every comparison with NaN is false.
Recap
ifruns a block when its condition is truthy;else ifandelseadd more branches, and only the first match runs.condition ? a : bchooses between two values. Keep it short.switchcompares one value with===against many cases. Rememberbreak.- Guard clauses check the bad cases first and
returnearly, which keeps code flat. - Validate every outside input before you make a decision with it, and test the edges.
Next, Loops: repeat work as often as you need, and build a number guessing game.
Test yourself
Five questions, picked at random from this lesson's question bank. Some ask you to choose an answer, some to predict what code prints, and some to write code and run it in the terminal. Get 4 of 5 right to pass. If you don't, read the explanations and try again: you get 5 different questions.