JAVASCRIPT / CONDITIONALS
Switch statements and fall-through risk
Predict exactly where a switch jumps in and where it stops, use fall-through deliberately, and keep case bodies from leaking into each other.
What you will learn
- Trace where a switch jumps in and how far it runs before break, return, or throw
- Stack case labels for shared behaviour instead of relying on a run-on case body
- Wrap a case body in braces when it declares let or const
- Spot case labels that can never match because switch compares with strict ===
Understanding Switch statements and fall-through risk
A switch statement is not a stack of independent branches. The value in parentheses is compared with === against each case expression from top to bottom, and the first match becomes a jump target: control lands on that label and then keeps running forward through the rest of the switch body, ignoring every case label it passes on the way. That one detail explains most switch surprises, including why the string "2" never matches case 2 and why a case body with no terminator quietly runs the code belonging to the case below it.
Execution stops only when it hits break, return, throw, or the closing brace of the switch. Fall-through is therefore the default behaviour, not a mistake the engine warns you about, which is why it is both a useful tool and a common source of silent bugs. Deliberate fall-through comes in two shapes: stacked case labels with nothing between them, which is self-documenting because there is no skipped code, and a case that runs statements and then continues into the next case, which needs an explicit comment because any reader will otherwise assume you forgot a break.
The entire switch body is a single block. Case labels do not open scopes, which is the same reason statements flow past them, so a let or const written under one case is visible to every other case in the switch. If control jumps past that declaration, the name still exists but is uninitialised, and reading it throws instead of returning undefined. Giving each case body its own { } fixes both the scope leak and the readability problem, and inside a function, returning from every case removes the break question altogether.
One more scoping trap for break itself: it applies to the nearest enclosing breakable statement, so a break written inside a switch that sits inside a loop ends the switch and lets the loop continue.
function labelFor(code) {
let label = "";
switch (code) {
case 1:
label = "start";
// bug: no break, so execution keeps going into case 2
case 2:
label = "stop";
break;
case 3:
label = "pause";
break;
default:
label = "no match";
}
return label;
}
console.log(labelFor(1));
console.log(labelFor(2));
console.log(labelFor(3));
console.log(labelFor("1"));A matching case is a jump target rather than a self-contained branch, so execution enters there and runs forward until break, return, throw, or the end of the switch.
Worked examples
Fall-through used on purpose
Stacked case labels let several values share one body without any hidden skipped code.
function daysInMonth(month, year) {
switch (month) {
case "apr":
case "jun":
case "sep":
case "nov":
return 30;
case "feb":
return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0 ? 29 : 28;
default:
return 31;
}
}
console.log(daysInMonth("apr", 2026));
console.log(daysInMonth("feb", 2024));
console.log(daysInMonth("feb", 2100));
console.log(daysInMonth("jan", 2026));Example explained
Line 1The four month labels have no statements between them, so all four jump targets lead to the same return 30.
Line 2This is safe fall-through: there is no code between the labels for a reader to wonder about.
Line 3return exits the function immediately, so not one case needs a break.
Line 4daysInMonth("feb", 2100) gives 28 because 2100 is divisible by 100 but not by 400.
One block, one scope
A let declared under one case belongs to the whole switch, so skipping the declaration makes the name unreadable.
function run(step) {
switch (step) {
case 1:
let msg = "first";
console.log(msg);
break;
case 2:
try {
console.log(msg);
} catch (err) {
console.log(err.name);
}
break;
}
}
run(1);
run(2);Example explained
Line 1let msg lives in the switch body's single block scope, not in case 1.
Line 2run(1) evaluates the declaration on its way through, so msg is initialised and logs first.
Line 3run(2) jumps straight to case 2 and never runs the declaration, leaving msg in its temporal dead zone.
Line 4Reading it throws a ReferenceError; wrapping each case body in { } gives each case its own scope and prevents this.
break belongs to the switch, not the loop
Shows why a break inside a case cannot stop the surrounding loop, and what does.
const events = ["click", "stop", "click", "scroll"];
let plain = 0;
for (const e of events) {
switch (e) {
case "click":
plain++;
break;
case "stop":
break;
}
}
console.log("plain break:", plain);
let labelled = 0;
scan: for (const e of events) {
switch (e) {
case "click":
labelled++;
break;
case "stop":
break scan;
}
}
console.log("labelled break:", labelled);Example explained
Line 1In the first loop, break under case "stop" closes only the switch, so iteration continues and both clicks are counted.
Line 2scan: names the for loop so it can be targeted by name.
Line 3break scan; leaves the loop itself, so only the click before "stop" is counted.
Line 4The break under case "click" is still needed in both loops to stop it running the "stop" body.
Important notes
default does not have to be last. If it appears above other cases and nothing matched, control enters default and then falls through into the cases below it until a break stops it.
Because matching is strict, case NaN: can never match, and a case holding an object or array only matches the exact same reference, not an equal-looking value.
Common mistakes
Skipping break on the case that happens to be last, then adding a new case underneath it later: the old case now silently executes the new body, so the bug appears in an unrelated feature.
Switching on a string from a form field, dataset attribute, or JSON value against numeric labels like case 1: every === comparison fails and only default runs.
Writing break inside a switch that sits in a loop and expecting the loop to end: the loop runs every remaining iteration because break closed the switch instead.
Try it yourself
Change, predict, then run
In a browser console, write medal(place) that switches over 1, 2, and 3, assigning "gold", "silver", and "bronze" to a variable with no break statements, and log medal(1) to see which value survives. Then terminate each case properly and confirm medal(1), medal(3), and medal("1") return gold, bronze, and your default.
Open the JavaScript workspaceCheck your understanding
What does this print, and why? switch (7) { default: console.log("default"); case 9: console.log("nine"); break; case 5: console.log("five"); }
- default then nine, because control jumps to default and keeps running until the break in case 9
- default only, because default finishes the switch the way a final else finishes an if chain
- nothing, because default has to be the last clause for it to be reachable
- default, nine, five, because every clause below the jump target runs
Show answer
default is just another jump target. No case equals 7, so control enters default, prints, and then continues forward into case 9's statements, where break ends the switch before case 5. Option 2 is tempting because default usually sits last, but default has no terminating power of its own; only break, return, throw, or the end of the block stops execution.