JavaScript Switch
The switch statement is a control-flow tool that compares one expression against a list of possible values, called case labels, and runs the code for whichever one matches — instead of relying on a long chain of if / else if statements. It shines when a single value, like a status code, a day of the week, or a user role, could equal one of several discrete options, because it reads top-to-bottom like a menu of outcomes rather than a nested tree of conditions. Under the hood it still performs simple comparisons, but the syntax keeps multi-branch logic flat, readable, and easy to extend as new cases show up.
Overview: How the Switch Statement Works
A switch statement evaluates its controlling expression exactly once and stores the result. It then walks through each case label, from top to bottom, and compares the stored result against each case’s value using strict equality (===). This is an important detail: switch never coerces types the way the loose equality operator == does, so the string "1" will not match case 1:. The first case whose value strictly equals the expression is where execution begins.
Once a match is found, JavaScript does not automatically stop after that one case’s statements — it keeps executing every statement below it, case labels included, until it hits a break statement, a return (inside a function), a throw, or simply runs out of statements in the switch block. This behavior is called fall-through, and it is by design: it lets you deliberately group several case labels so they share one block of code (you’ll see this in the examples below). The trade-off is that forgetting a break is one of the most common switch bugs, covered later in Common Mistakes.
If none of the case values match, JavaScript looks for a default label and runs the code there. default is optional, and technically it can be placed anywhere in the switch block (not only at the end) — if it isn’t last, execution still only jumps to it when no case matches, but fall-through can carry it into whatever comes after it in source order. By convention, and for readability, default is almost always written last.
One subtlety that trips up developers coming from other languages: the entire body of a switch statement — every case together — is a single lexical block. If you declare a variable with let or const directly inside one case without wrapping it in its own { } braces, that variable is actually scoped to the whole switch, not just that one case. That can cause a SyntaxError if another case tries to declare a variable with the same name. See Mistake 2 below for the fix.
Syntax
const fruit = "apple";
switch (fruit) {
case "banana":
console.log("It's a banana");
break;
case "apple":
console.log("It's an apple");
break;
default:
console.log("Unknown fruit");
}
| Part | Meaning |
|---|---|
switch (expression) |
The value being tested. Evaluated once, at the start. |
case value: |
A candidate value compared to the expression using ===. If it matches, execution starts here. |
break; |
Exits the switch immediately. Without it, execution falls through into the next case. |
default: |
Runs when no case matches. Optional, conventionally placed last. |
Examples
Example 1: Mapping a number to a name
const day = 3;
let dayName;
switch (day) {
case 1:
dayName = "Monday";
break;
case 2:
dayName = "Tuesday";
break;
case 3:
dayName = "Wednesday";
break;
case 4:
dayName = "Thursday";
break;
case 5:
dayName = "Friday";
break;
case 6:
dayName = "Saturday";
break;
case 7:
dayName = "Sunday";
break;
default:
dayName = "Invalid day";
}
console.log(dayName);
Output:
Wednesday
The expression day is evaluated once (it’s 3), then compared to each case in order. It strictly equals case 3, so dayName is set to "Wednesday" and break stops execution before it ever reaches case 4.
Example 2: Grouping cases with intentional fall-through
const month = "March";
let quarter;
switch (month) {
case "January":
case "February":
case "March":
quarter = "Q1";
break;
case "April":
case "May":
case "June":
quarter = "Q2";
break;
case "July":
case "August":
case "September":
quarter = "Q3";
break;
case "October":
case "November":
case "December":
quarter = "Q4";
break;
default:
quarter = "Unknown month";
}
console.log(`${month} is in ${quarter}`);
Output:
March is in Q1
Here, three empty case labels in a row ("January", "February", "March") share the same code because none of the first two have a break — execution falls straight through them until it reaches the statement attached to "March". This is the useful, intentional side of fall-through: it avoids repeating quarter = "Q1"; three times.
Example 3: Range checks with switch(true)
function describeStatus(status) {
switch (true) {
case status >= 200 && status < 300:
return "Success";
case status >= 300 && status < 400:
return "Redirection";
case status >= 400 && status < 500:
return "Client Error";
case status >= 500 && status < 600:
return "Server Error";
default:
return "Unknown Status";
}
}
console.log(describeStatus(201));
console.log(describeStatus(404));
console.log(describeStatus(503));
console.log(describeStatus(999));
Output:
Success
Client Error
Server Error
Unknown Status
Normally each case holds a single value, but here the switch expression itself is the literal true, and each case is a boolean condition. JavaScript still compares with strict equality, so a case only runs when its condition evaluates to exactly true. This is a well-known idiom for turning a switch into a readable range-checker, something a plain case list can’t do on its own.
Under the Hood: Step by Step
It helps to think of what the JavaScript engine actually does when it hits a switch:
- Evaluate the controlling expression once and keep the resulting value.
- Scan the case labels in source order, evaluating each case’s own expression as it goes (case expressions are not pre-computed ahead of time).
- Compare the switch value to each case value with
===— no type coercion, unlike==. - On the first match, jump into that case and execute statements downward, ignoring further case labels as boundaries (they’re just markers, not gates) until a
break,return,throw, or the end of the block is reached. - If nothing matched, jump to
defaultif one exists; otherwise do nothing and continue after the switch.
The strict-equality rule is easy to forget, so it’s worth seeing directly:
function checkType(value) {
switch (value) {
case 0:
return "matched number 0";
case "0":
return "matched string '0'";
case false:
return "matched boolean false";
default:
return "no match";
}
}
console.log(checkType(0));
console.log(checkType("0"));
console.log(checkType(false));
console.log(checkType(""));
Output:
matched number 0
matched string '0'
matched boolean false
no match
Even though 0, "0", and false are all loosely equal to each other with ==, switch tells them apart perfectly because it uses ===. The empty string "" matches none of the three cases and falls to default.
Common Mistakes
Mistake 1: Forgetting a break statement
Leaving out break is the single most common switch bug — the code stays perfectly valid, it just silently runs more statements than you intended.
function getSeason(month) {
let season;
switch (month) {
case "December":
case "January":
case "February":
season = "Winter";
case "March":
case "April":
case "May":
season = "Spring";
break;
default:
season = "Unknown";
}
return season;
}
console.log(getSeason("January"));
Output:
Spring
This looks like it should print "Winter", but there is no break after season = "Winter";, so execution falls straight through into the "March" group and overwrites season with "Spring" before finally hitting break. The fix is to add the missing break:
function getSeason(month) {
let season;
switch (month) {
case "December":
case "January":
case "February":
season = "Winter";
break;
case "March":
case "April":
case "May":
season = "Spring";
break;
default:
season = "Unknown";
}
return season;
}
console.log(getSeason("January"));
Output:
Winter
Mistake 2: Redeclaring let/const across case clauses
Because every case in a switch shares one lexical block, declaring the same let or const name in two different cases without braces is a redeclaration in the *same* scope, and it throws a SyntaxError at parse time — before the code ever runs:
switch (x) {
case 1:
let message = "one";
console.log(message);
break;
case 2:
let message = "two"; // SyntaxError: Identifier 'message' has already been declared
console.log(message);
break;
}
Both message declarations live in the same block (the switch body), so the engine treats the second one as a duplicate declaration, exactly as if they’d been written back-to-back outside any switch. The fix is to give each case its own block scope by wrapping it in curly braces:
function process(x) {
switch (x) {
case 1: {
let message = "one";
console.log(message);
break;
}
case 2: {
let message = "two";
console.log(message);
break;
}
default: {
console.log("other");
}
}
}
process(1);
process(2);
Output:
one
two
Wrapping each case body in { } gives it its own scope, so both cases can freely declare a variable named message without colliding.
Best Practices
- Always include a
break(orreturn) at the end of each case unless you are intentionally grouping cases for fall-through — and if you do fall through on purpose, add a short comment so future readers know it’s intentional. - Put
defaultlast, even though the language allows it anywhere, so the fallback behavior reads naturally at the bottom. - Wrap each case body in
{ }whenever it declares alet,const, or a function, to avoid scope collisions and keep each branch self-contained. - Prefer
returnoverbreakwhen the switch lives inside a function whose only job is to produce a value — it’s shorter and impossible to accidentally fall through. - Remember switch uses strict equality (
===); don’t rely on it to coerce types the way loose comparisons do. - For simple 2-3 branch logic, a plain
if / elseis often clearer; reach forswitchonce you have several discrete values to compare against one expression. - Consider an object literal or a
Mapas an alternative to a long switch when every case just maps a key to a fixed value with no extra logic — it can be more concise and avoids fall-through risk entirely.
Practice Exercises
- Write a function
trafficLightAction(color)that takes"red","yellow", or"green"and uses aswitchto return"Stop","Slow down", or"Go"respectively, and"Unknown signal"for anything else. - Write a function
gradeLetter(score)that uses theswitch (true)pattern to return"A"for scores 90 and above,"B"for 80-89,"C"for 70-79, and"F"below 70. - Take the following buggy snippet, identify the missing keyword, and rewrite it so that calling
describeNumber(-5)logs only"Negative"instead of logging both"Negative"and"Zero or positive":function describeNumber(n) { switch (true) { case n < 0: console.log("Negative"); case n >= 0: console.log("Zero or positive"); } }
Summary
switchevaluates an expression once and compares it to eachcasevalue using strict equality (===), with no type coercion.- Without a
break,return, orthrow, execution falls through into the next case — useful for intentionally grouping cases, dangerous when forgotten. defaultruns when nothing else matches and is conventionally placed last, though the language allows it anywhere.- The whole switch body shares one lexical scope, so
let/constdeclared directly in a case can collide with another case’s declaration unless each case is wrapped in its own{ }block. - The
switch (true)idiom turns case labels into boolean conditions, letting you express range checks that a plain switch can’t. - For simple key-to-value mappings without extra logic, an object literal or
Mapcan be a cleaner alternative to a long switch.
