At the end of the previous lesson a very specific awkwardness turned up: inside the loop that walked the backlog you had to repeat the same else if chain over and over to turn priority into a numeric weight. That pattern —a single variable compared against a list of fixed values— is so frequent that JavaScript has a dedicated construct for it: the switch. In this lesson you will learn its syntax, understand why it compares with === and what that implies, tell deliberate fall-through apart from accidental fall-through, see why a case sometimes needs braces, and get to know the two alternatives it competes with: the usual else if chain and the lookup object, which will be your favorite tool from Module 4 onward.
Contents
- When a
switchimproves on anelse ifchain switchsyntax- How it compares: strict equality
default: the fallback casebreakand accidental fall-through- Deliberate fall-through: grouping cases
- Variable scope inside a
case - The
switch (true)pattern and why to avoid it switchvsif/else ifvs lookup object- Case study: a display label for the status
- Case study: priority to color and to weight
- Common Mistakes and Tips
- Exercises
- Conclusion
- When a
switch improves on an else if chain
switch improves on an else if chainRecall the fragment that kept repeating in lesson 02-02:
let weight;
if (priorities[i] === 'high') {
weight = 3;
} else if (priorities[i] === 'medium') {
weight = 2;
} else if (priorities[i] === 'low') {
weight = 1;
} else {
weight = 0;
}There is something mechanical about that code: the left-hand side of the comparison is always the same and so is the operator. The only thing that changes is the value on the right. That repetition is what the switch removes: you write the expression once and then only the list of values.
A switch is the right tool when these three conditions hold:
- You are comparing a single expression.
- You are comparing by exact equality against specific values, not by ranges or compound conditions.
- There are three or more cases. With two, an
if/elseis shorter and clearer.
If any of them fails, the correct answer is still an if/else if.
switch syntax
switch syntaxconst priority = 'medium';
let weight;
switch (priority) {
case 'high':
weight = 3;
break;
case 'medium':
weight = 2;
break;
case 'low':
weight = 1;
break;
default:
weight = 0;
console.error(`Unknown priority: "${priority}"`);
}
console.log(`Weight: ${weight}`); // Weight: 2The pieces:
| Element | Purpose |
|---|---|
switch (expression) |
Evaluates the expression once only and stores its value |
case value: |
Labels an entry point; if it matches, execution jumps here |
break |
Leaves the switch and continues after the closing brace |
default: |
The entry point when no case has matched |
And the detail that explains everything: case is not a condition, it is a jump label. The switch does not evaluate case by case like an if/else if; it looks for the point to enter and, from there, runs downward until it finds a break or reaches the end of the block. Understanding this makes the rest of the lesson obvious.
flowchart TD
A["switch (priority)"] --> B{"=== 'high'?"}
B -->|yes| C["weight = 3"] --> Z["break → exit"]
B -->|no| D{"=== 'medium'?"}
D -->|yes| E["weight = 2"] --> Z
D -->|no| F{"=== 'low'?"}
F -->|yes| G["weight = 1"] --> Z
F -->|no| H["default: weight = 0"] --> Z
Z --> I["The program continues"]
A note on style: the whole block goes between braces and the case clauses do not have braces of their own by default. The usual indentation puts the case one level inside the switch and its body one level further in.
- How it compares: strict equality
The switch compares the value of the expression with each case using ===, the strict equality you adopted as a rule in Type Conversion and Comparisons. There is no type conversion of any kind.
The practical consequence matters when the data comes from a form, where everything is text:
const formHours = '12'; // ← a string, it comes from an <input>
switch (formHours) {
case 12:
console.log('Twelve hours');
break;
default:
console.log('It does not match any case'); // ← this one runs
}'12' === 12 is false, so it goes into default. The fix is the one you already know: convert explicitly before comparing.
The fact that it uses === is good news: it removes the surprises of implicit conversion. But it also inherits its one oddity:
const value = NaN;
switch (value) {
case NaN:
console.log('It never gets here');
break;
default:
console.log('NaN is not even equal to itself'); // ← this one runs
}NaN === NaN is false, so case NaN is unreachable. To detect a NaN you still need Number.isNaN() inside an if.
And for the same reason, a switch over objects or arrays compares references, not contents, which is almost never what you want. In practice, the switch is a tool for primitive values: strings and numbers.
default: the fallback case
default: the fallback casedefault runs when no case matches. It is the equivalent of the final else in a chain, and the same rule from lesson 02-01 applies: always include it, even if only to log that an unexpected value has arrived.
Two peculiarities:
It does not have to go last. It is legal to put it in the middle, and then it does need a break so that execution does not continue into the following case clauses. It is legal, but baffling for the reader: always put it last.
The last block can omit the break. If default is the last thing, there is nothing below to fall into. Even so, many teams require writing it anyway: the day someone adds a case after it, the break will already be there. It is a cheap habit that prevents an expensive bug.
break and accidental fall-through
break and accidental fall-throughSince the switch runs downward from the entry point, forgetting a break makes execution spill over into the next case. That is called fall-through.
// ✗ The break is missing in 'high'
const priority = 'high';
let weight;
switch (priority) {
case 'high':
weight = 3;
case 'medium':
weight = 2;
break;
case 'low':
weight = 1;
break;
}
console.log(weight); // 2 ← not 3!The trace: it enters at case 'high', assigns weight = 3, finds no break, continues downward, enters the body of 'medium' without checking anything, assigns weight = 2 and only then leaves. The result is silently wrong.
This bug is especially treacherous because it produces no warning: JavaScript treats fall-through as a feature, not a defect. The tool that does warn you is a linter such as ESLint, with its no-fallthrough rule, which you will see in Code Quality.
In the meantime, the protection is a habit: write the break immediately after opening the case, before you fill in the body.
- Deliberate fall-through: grouping cases
The same mechanism, used on purpose, is the best feature of the switch: several case clauses in a row, with no body between them, share the same block.
In Nómada Tasks there is one question that comes up constantly: is this task still open?
const status = 'in-progress';
let isStillOpen;
switch (status) {
case 'pending':
case 'in-progress':
isStillOpen = true;
break;
case 'done':
isStillOpen = false;
break;
default:
isStillOpen = false;
console.error(`Unrecognized status: "${status}"`);
}
console.log(isStillOpen); // truecase 'pending': has no body of its own, so execution falls straight into the body of case 'in-progress':. It reads very naturally: "if it is pending or in progress, then...".
When the fall-through does carry code in the intermediate case, the intention stops being obvious and has to be declared with a comment. That is the universal convention:
switch (alertLevel) {
case 'critical':
console.error('Call Marta on the phone');
// fall through — a critical alert also sends an email
case 'important':
console.warn('Send an email to the assignee');
break;
case 'informational':
console.log('It only shows up on the board');
break;
}With 'critical' the first two actions run. Without the // fall through comment, anyone reviewing the code would take it for a forgotten break and would "fix" it, breaking the functionality. Every fall-through with intermediate code must carry that comment.
- Variable scope inside a
case
caseHere is a subtlety that surprises a lot of people: all the case clauses of a switch share a single block scope, that of the switch itself. There is no scope per case.
That causes a real error:
// ✗ SyntaxError: Identifier 'label' has already been declared
switch (status) {
case 'pending':
const label = 'Not started';
console.log(label);
break;
case 'in-progress':
const label = 'Under way'; // ✗ same name, same scope
console.log(label);
break;
}And another, subtler one, which does not fail when you write it but when you run it:
// ✗ ReferenceError: Cannot access 'weight' before initialization
switch (priority) {
case 'medium':
console.log(weight); // the declaration below exists, but it is not initialized yet
break;
case 'high':
const weight = 3;
break;
}It is the temporal dead zone of let and const that you saw in Variables and Data Types, applied to the whole block of the switch.
The fix is to give each case its own braces:
switch (status) {
case 'pending': {
const label = 'Not started';
console.log(label);
break;
}
case 'in-progress': {
const label = 'Under way'; // ✓ its own scope, no conflict
console.log(label);
break;
}
}Notice where the break goes: inside the braces. The braces delimit the scope, not the case.
Practical rule: if a case declares variables, give it braces. If it only assigns to variables already declared outside —which is the most common use— they are not needed.
- The
switch (true) pattern and why to avoid it
switch (true) pattern and why to avoid itswitch compares by equality, so in principle it is no use for ranges. There is a trick to force it: put true as the expression and full conditions in each case.
// △ It works, but do not write it
const estimatedHours = 12;
let workload;
switch (true) {
case estimatedHours <= 0 || estimatedHours > 40:
workload = 'invalid';
break;
case estimatedHours <= 4:
workload = 'light';
break;
case estimatedHours <= 15:
workload = 'medium';
break;
default:
workload = 'heavy';
}
console.log(workload); // mediumThe mechanism is consistent: the expression is true, each case is evaluated and produces true or false, and execution enters at the first one whose value is true.
And yet there are three solid reasons not to use it:
- It lies about what it does. A reader expects
switch (x)to compare values ofx; hereswitch (true)compares nothing useful and you have to read everycaseto follow the logic. - It is exactly an
if/else ifwith worse syntax. Compare the equivalent version: same conditions, same order, same result, four fewer characters of noise per case and nobreakto forget. - It is fragile. A forgotten
breakin this version does not jump to another equivalent case, it runs an assignment whose condition was false.
// ✓ The correct version
if (estimatedHours <= 0 || estimatedHours > 40) {
workload = 'invalid';
} else if (estimatedHours <= 4) {
workload = 'light';
} else if (estimatedHours <= 15) {
workload = 'medium';
} else {
workload = 'heavy';
}The complete rule: switch for exact values, if/else if for ranges and compound conditions. There are no exceptions worth making.
switch vs if/else if vs lookup object
switch vs if/else if vs lookup objectThere is a third way to solve these conversions, and it is the one that will dominate the rest of the course: a lookup object, a table that associates each input value with its result.
Objects are studied in depth in Introduction to Objects; here the idea is enough: they are written between braces as key: value pairs and read with square brackets.
const WEIGHTS = { high: 3, medium: 2, low: 1 };
const priority = 'medium';
const weight = WEIGHTS[priority] ?? 0;
console.log(weight); // 2Four lines become two, and the ?? you learned in Basic Operators covers the default case: if priority is not in the table, WEIGHTS[priority] gives undefined and ?? replaces it with 0.
The full comparison:
| Criterion | if / else if |
switch |
Lookup object |
|---|---|---|---|
| Comparing a variable with exact values | △ Verbose | ✓ Ideal | ✓ Ideal |
| Ranges and compound conditions | ✓ The only option | ✗ Only with the switch(true) trick |
✗ No use |
| Running different actions per case | ✓ Yes | ✓ Yes | △ Needs functions (Module 3) |
| Grouping several values into one case | △ With || |
✓ Deliberate fall-through | △ Repeat the key |
| Adding a new case | Edit code | Edit code | Add a line of data |
| Risk of forgetting something | Forgetting the else |
Forgetting the break |
None |
| Default value | else |
default |
?? |
| Readability with 10+ cases | ✗ Poor | △ Acceptable | ✓ Excellent |
The practical conclusion you will apply from now on:
- Mapping one value to another value (status → label, priority → color): lookup object.
- Running different actions depending on a value:
switch. - Deciding by ranges or compound conditions:
if / else if.
The fact that the lookup object wins in so many boxes does not make the switch useless: it shows up constantly in real code —especially in Redux reducers, which you will see in Module 10— and you have to be able to read and write it fluently.
- Case study: a display label for the status
The project's internal values ('pending', 'in-progress', 'done') are designed for the code, not for Marta. The interface needs presentable text, and here the switch does more than a simple translation: it also picks the icon and the console alert level.
const statuses = ['in-progress', 'pending', 'pending', 'done', 'in-progress', 'pending'];
const titles = [
'Redesign the multipurpose room',
'Signage for the screen-printing workshop',
'Update the bookings website',
'Screen-printing ink inventory',
'Bookbinding guide for residents',
'Carpentry workshop quote'
];
for (let i = 0; i < statuses.length; i++) {
let label;
let icon;
switch (statuses[i]) {
case 'pending':
label = 'Not started';
icon = '○';
break;
case 'in-progress':
label = 'Under way';
icon = '◐';
break;
case 'done':
label = 'Completed';
icon = '●';
break;
default:
label = 'Unknown status';
icon = '?';
console.error(`Task ${i + 1}: invalid status "${statuses[i]}"`);
}
console.log(`${icon} ${titles[i]} — ${label}`);
}Output:
◐ Redesign the multipurpose room — Under way ○ Signage for the screen-printing workshop — Not started ○ Update the bookings website — Not started ● Screen-printing ink inventory — Completed ◐ Bookbinding guide for residents — Under way ○ Carpentry workshop quote — Not started
Check that the three conditions from section 1 hold: a single expression (statuses[i]), exact equality and three cases. The switch is well used. And notice that let label; and let icon; are declared inside the loop but outside the switch: inside the loop because they change on every pass, outside the switch because they have to be used after it finishes.
- Case study: priority to color and to weight
Rule R10 asks for tasks to be highlighted visually. Each priority has a color in the interface and a numeric weight for sorting the board, and a single switch can assign both things at once in each branch: that is an advantage over the lookup object, which would need one table per field.
const priorities = ['high', 'medium', 'high', 'low', 'medium', 'high'];
const hours = [12, 6, 14, 3, 8, 5];
let totalEffort = 0;
for (let i = 0; i < priorities.length; i++) {
let color;
let weight;
switch (priorities[i]) {
case 'high':
color = '#c0392b'; // red
weight = 3;
break;
case 'medium':
color = '#e67e22'; // orange
weight = 2;
break;
case 'low':
color = '#27ae60'; // green
weight = 1;
break;
default:
color = '#7f8c8d'; // gray
weight = 0;
console.error(`Invalid priority: "${priorities[i]}"`);
}
totalEffort += weight * hours[i];
console.log(`${priorities[i].padEnd(6)} ${color} weight ${weight} effort ${weight * hours[i]}`);
}
console.log(`Weighted effort of the backlog: ${totalEffort}`);Output:
high #c0392b weight 3 effort 36 medium #e67e22 weight 2 effort 12 high #c0392b weight 3 effort 42 low #27ae60 weight 1 effort 3 medium #e67e22 weight 2 effort 16 high #c0392b weight 3 effort 15 Weighted effort of the backlog: 124
padEnd(6) pads the text with spaces up to 6 characters, so that the columns line up in the console.
Now, the same logic with lookup objects, so you can see where the course is heading:
const COLORS = { high: '#c0392b', medium: '#e67e22', low: '#27ae60' };
const WEIGHTS = { high: 3, medium: 2, low: 1 };
for (let i = 0; i < priorities.length; i++) {
const color = COLORS[priorities[i]] ?? '#7f8c8d';
const weight = WEIGHTS[priorities[i]] ?? 0;
console.log(`${priorities[i].padEnd(6)} ${color} weight ${weight}`);
}Twenty lines become four, and adding an 'urgent' priority would mean adding a pair to each table instead of a case to each switch. When configuration data is separated from the logic that uses it, the code shrinks. That idea runs through the rest of the course.
Common Mistakes and Tips
Forgetting the break. The number-one switch bug. It shows up as an incorrect final value, with no error message. Always write it right after opening the case.
Relying on type conversion. switch uses ===. If the expression may arrive as text from a form, convert it first: switch (Number(field)).
Putting conditions in the case clauses. case hours > 10: compares the value of the expression with the resulting boolean, which is almost never what you want. For ranges, if/else if.
Declaring const or let in several case clauses without braces. It causes a SyntaxError from redeclaration or a ReferenceError from the temporal dead zone. Put braces on the cases that declare variables.
Omitting the default. Without it, an unexpected value leaves the variables as undefined and the failure surfaces later, somewhere else. Always include it, even if it only logs the error.
Using a switch with two cases. if/else is shorter and more readable. The switch starts to pay off from three cases onward.
Using a switch with objects or arrays. It compares references, not content: two arrays with the same elements never match. The switch is for primitives.
Tip: order the case clauses by frequency or alphabetically. It does not affect performance —the engine optimizes the jump— but it makes it easier to find a specific case and to notice a missing one.
Tip: when a switch goes past ten cases or only maps values, turn it into a lookup object. That is the natural evolution, and with what you learn in Module 4 you will be able to do it effortlessly.
Exercises
Exercise 1 — Warning window per priority
Taller Nómada defines a recommended warning window based on priority: 'high' warns 3 days in advance, 'medium' 7 and 'low' 14. Write a switch that, from priority, assigns warningDays, and use a default that assigns 7 days and logs an error. Test it with 'high', 'low' and 'super-urgent'.
Exercise 2 — Grouping statuses with fall-through
Write a switch over status that assigns two variables: onBoard (a boolean: whether the task should still be displayed on the active board) and message. The statuses 'pending' and 'in-progress' share the same treatment (onBoard = true), while 'done' leaves the board. Use deliberate fall-through so you do not repeat code, and walk the array ['in-progress', 'pending', 'done', 'archived'] with a loop, checking the default too.
Exercise 3 — From switch to lookup object
This switch turns a workshop code into its full name. Rewrite it using a lookup object and ??, and explain in which cases the switch version would still be preferable.
let workshopName;
switch (code) {
case 'SCR': workshopName = 'Screen printing'; break;
case 'BND': workshopName = 'Bookbinding'; break;
case 'CAR': workshopName = 'Carpentry'; break;
case 'COW': workshopName = 'Coworking'; break;
default: workshopName = 'Unclassified';
}Solutions
Exercise 1
const priority = 'high';
let warningDays;
switch (priority) {
case 'high':
warningDays = 3;
break;
case 'medium':
warningDays = 7;
break;
case 'low':
warningDays = 14;
break;
default:
warningDays = 7;
console.error(`Unrecognized priority: "${priority}". The standard window is applied.`);
}
console.log(`Warn ${warningDays} day(s) in advance.`);
// Warn 3 day(s) in advance.With 'low' it gives 14 days. With 'super-urgent' it goes into default, logs the error and applies 7 days: the program does not stop and makes a reasonable decision, which is exactly what a well-written default should do. A default that left warningDays unassigned would propagate an undefined into a later calculation, where the error would appear with no clue as to its origin.
Exercise 2
const testStatuses = ['in-progress', 'pending', 'done', 'archived'];
for (let i = 0; i < testStatuses.length; i++) {
const status = testStatuses[i];
let onBoard;
let message;
switch (status) {
case 'pending':
case 'in-progress':
onBoard = true;
message = 'Still on the active board';
break;
case 'done':
onBoard = false;
message = 'Moved to the history';
break;
default:
onBoard = false;
message = 'Unrecognized status: hidden to be safe';
console.error(`Invalid status: "${status}"`);
}
console.log(`${status.padEnd(12)} → onBoard=${onBoard} · ${message}`);
}Output:
in-progress → onBoard=true · Still on the active board pending → onBoard=true · Still on the active board done → onBoard=false · Moved to the history archived → onBoard=false · Unrecognized status: hidden to be safe
The deliberate fall-through is in case 'pending':, which has no body and falls into the one for 'in-progress'. It is the clean use of the feature: two labels, a single block, zero duplication. The if alternative would be if (status === 'pending' || status === 'in-progress'), just as valid but with the variable repeated.
Notice the default decision too: faced with an unknown status we hide the task instead of showing it. It is the conservative policy you already applied in the transition validation in lesson 02-01: when in doubt, reject.
Exercise 3
const WORKSHOPS = {
SCR: 'Screen printing',
BND: 'Bookbinding',
CAR: 'Carpentry',
COW: 'Coworking'
};
const code = 'BND';
const workshopName = WORKSHOPS[code] ?? 'Unclassified';
console.log(workshopName); // BookbindingEleven lines become three plus the table, and adding a new workshop means adding one line of data, not a branch of code. The WORKSHOPS table can also be reused: to render a dropdown in the form, or to validate that a code exists (WORKSHOPS[code] === undefined).
When the switch would still be preferable:
- When each case runs different actions rather than just returning a value: opening a window, logging a warning, updating three variables at once. An object can store functions for that, but it is a technique from Module 3 onward.
- When some cases have to share a block through fall-through, as in exercise 2.
- When the keys are not simple strings or numbers, although in practice this is rare.
In short: the lookup object wins when the answer is a piece of data; the switch wins when the answer is a behavior.
Conclusion
You now know JavaScript's third selection structure. You know that a switch evaluates its expression once, compares it with each case using === with no conversion whatsoever, and that the case clauses are not conditions but jump labels: execution enters at the one that matches and runs down to the first break. From that mechanism follow the two faces of fall-through —the accidental one, which silently assigns wrong values, and the deliberate one, which groups several values into a single block and has to be documented with a comment— and also the need to put braces on the case clauses that declare variables, because the whole switch shares a single scope.
You know when to use it: a single expression, exact values, three or more cases. And when not to: for ranges, if/else if, because the switch (true) trick only dresses an if up as something else. You have also seen the third way —the lookup object with ?? for the default case—, which reduces any value-to-value conversion to two lines and which will be your default choice as soon as you master objects.
Applied to the project, you can already translate the internal statuses into presentable labels for Marta and turn the priority into a color and a weight for sorting the board.
One awkwardness you have been carrying since the previous lesson is still open: the loops still walk all six tasks even when the answer is already known at the first one, and you still do not know how to cross two lists —the three assignees against the six tasks— without writing counters by hand. Both are solved in Flow Control: break, continue and Nested Loops, where you will learn to interrupt a loop at exactly the right moment, to skip the iterations you do not care about and to build the full matrix of Taller Nómada's board.
JavaScript Course: From Beginner to Advanced
Module 1: Introduction to JavaScript
- What Is JavaScript?
- Setting Up Your Development Environment
- Your First JavaScript Program
- JavaScript Syntax and Basic Concepts
- Variables and Data Types
- Basic Operators
- Type Conversion and Comparisons
- The Course Project: Nómada Tasks
Module 2: Control Structures
- Conditional Statements
- Loops: for, while, do-while
- Switch Statements
- Flow Control: break, continue and Nested Loops
- Error Handling with try-catch
Module 3: Functions
- Defining and Calling Functions
- Function Expressions and Arrow Functions
- Parameters and Return Values
- Scope and Closures
- Hoisting and the Execution Context
- Higher-Order Functions
- Recursion
Module 4: Objects and Arrays
- Introduction to Objects
- Object Methods and the
thisKeyword - Arrays: Basics and Methods
- Iterating over Arrays
- Searching, Sorting and Aggregating Data: find, sort and reduce
- Array Destructuring
- Object Destructuring, Spread and Rest
- JSON and Copying Objects
Module 5: Advanced Objects and Functions
- Prototypes and Inheritance
- Classes and Object-Oriented Programming
- Encapsulation: Getters, Setters and Private Fields
- Modules: Import and Export
- Asynchronous JavaScript: Callbacks
- Promises and Async/Await
- The Event Loop and the Microtask Queue
- Iterators and Generators
Module 6: The Document Object Model (DOM)
- Introduction to the DOM
- Selecting and Manipulating DOM Elements
- Handling Events
- Propagation, Delegation and Custom Events
- Creating and Removing DOM Elements
- Rendering Lists and HTML Templates
- Handling and Validating Forms
Module 7: Browser APIs and Advanced Topics
- Local and Session Storage
- The Fetch API and AJAX
- Robust Requests: Errors, Timeouts and AbortController
- WebSockets
- Service Workers and Progressive Web Apps (PWAs)
- Essential Browser APIs
- Introduction to WebAssembly
Module 8: Testing and Debugging
- Debugging JavaScript
- Code Quality: ESLint, Prettier and Conventions
- Unit Testing with Jest
- Test Doubles: Mocks, Stubs and Spies
- Integration Testing
- End-to-End Testing with Cypress
Module 9: Performance and Optimization
- Measure Before You Optimize: DevTools and Web Vitals
- Optimizing JavaScript Performance
- Memory Management
- Efficient DOM Manipulation
- Lazy Loading and Code Splitting
Module 10: JavaScript Frameworks and Libraries
- Why Frameworks Exist
- Introduction to React
- State Management with Redux
- Vue.js Basics
- Angular Basics
- Choosing the Right Framework
