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

  1. When a switch improves on an else if chain
  2. switch syntax
  3. How it compares: strict equality
  4. default: the fallback case
  5. break and accidental fall-through
  6. Deliberate fall-through: grouping cases
  7. Variable scope inside a case
  8. The switch (true) pattern and why to avoid it
  9. switch vs if/else if vs lookup object
  10. Case study: a display label for the status
  11. Case study: priority to color and to weight
  12. Common Mistakes and Tips
  13. Exercises
  14. Conclusion

  1. When a switch improves on an else if chain

Recall 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/else is shorter and clearer.

If any of them fails, the correct answer is still an if/else if.

  1. switch syntax

const 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: 2

The 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.

  1. 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.

switch (Number(formHours)) {
  case 12:
    console.log('Twelve hours');   // ✓ now it does
    break;
}

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.

  1. default: the fallback case

default 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.

default:
  weight = 0;
  console.error(`Unknown priority: "${priority}"`);

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.

  1. break and accidental fall-through

Since 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.

  1. 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);   // true

case '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.

  1. Variable scope inside a case

Here 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.

  1. The switch (true) pattern and why to avoid it

switch 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);   // medium

The 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:

  1. It lies about what it does. A reader expects switch (x) to compare values of x; here switch (true) compares nothing useful and you have to read every case to follow the logic.
  2. It is exactly an if/else if with worse syntax. Compare the equivalent version: same conditions, same order, same result, four fewer characters of noise per case and no break to forget.
  3. It is fragile. A forgotten break in 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.

  1. switch vs if/else if vs lookup object

There 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);   // 2

Four 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.

  1. 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.

  1. 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);   // Bookbinding

Eleven 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

Module 2: Control Structures

Module 3: Functions

Module 4: Objects and Arrays

Module 5: Advanced Objects and Functions

Module 6: The Document Object Model (DOM)

Module 7: Browser APIs and Advanced Topics

Module 8: Testing and Debugging

Module 9: Performance and Optimization

Module 10: JavaScript Frameworks and Libraries

Module 11: Final Project

© Copyright 2026. All rights reserved