A switch is a code smell
Pierluigi Pesenti

Pierluigi Pesenti @oaxoa

About: Senior software engineer, engineering manager, technical writer, artist, cook, and writer.

Location:
Zurich
Joined:
Sep 5, 2018

A switch is a code smell

Publish Date: Jun 1
8 6

A switch statement smells.
For several reasons, and you should never use it.

Let's make some arguments:

  1. It is verbose and *WET*
  2. Counter-intuitive fall-trough behavior
  3. It is a statement, not an expression

Let's get started with the main argument of this article:

It is verbose and WET

Look at the following example:

Example 1

const quantity = 1;

switch (quantity) {
    case 0:
        console.log('You have none. Sorry');
        break;
    case 1:
        console.log('You have some. Keep going!');
        break;
    case 2:
        console.log('Someone here is rich!');
        break;
    default:
        console.log('No cases matched');
}
Enter fullscreen mode Exit fullscreen mode

It shows a very common use of a switch. In this example the intent is to log different messages based on a quantity.

Repetitions are everywhere:

  1. Every value is prefixed with case;
  2. Every log message is wrapped in a console.log call;
  3. Every case block is ended with a break;

What happens in the example is basic mapping.
We should use a map to map things.

In JS an object is a map. A Map class is also available with different APIs.

Here is the revised code:

Revised 1

const map = {
    0: 'You have none. Sorry',
    1: 'You have some. Keep going!',
    2: 'Someone is rich!',
};
console.log(map[quantity] ?? 'No cases matched');
Enter fullscreen mode Exit fullscreen mode

Benefits

  1. The mapping overhead goes down to the bare minimum, the association case -> output is addressed by a simple colon. It can't get *DRY*er than that (We could eventually use tuples)
  2. The log is invoked only once with the proper map index as content
  3. A nullish coalescing operator (??) or a logical OR (||) takes care of the default value in the case of no matches
  4. All the repetitions found in the original example are gone. After all, all we wanna do is log something
  5. No break or default keywords are needed
  6. the intent of mapping a quantity to a message is much more clear
  7. No 2-deep nesting needed
  8. 161 chars vs 319 (helps with readability)
  9. Last but not least while the invocation of the console.log is still a statement, its argument is now an expression. Which can be easily extracted and given a name. The declaration of the map is technically also a statement here, but it could easily be an expression if inlined or passed as an argument (see Chapter 3).

Counter-intuitive fall-trough behavior

A switch statement can do several different things based on how we use it (and this is not necessarily a good thing). It is broken by design and can be a nightmare to decode.

Let's have a look at an example that leverages the fall-trough directly from MDN doc page:

Example 2

const animal = 'Giraffe';
switch (animal) {
    case 'Cow':
    case 'Giraffe':
    case 'Dog':
    case 'Pig':
        console.log('This animal is not extinct.');
    break;
    case 'Dinosaur':
    default:
        console.log('This animal is extinct.');
}

Enter fullscreen mode Exit fullscreen mode

This is a common pattern to have several values triggering the same behaviour, but it works because of a secret rule that says: "if you leverage fall-trough you still need a break after doing something". Omitting the break after the first console.log would trigger the second too, despite the case not being 'Dinosaur' or unmatched (default).

Let's verify this behaviour with a distilled example:

Example 3

switch (1) {
  case 1:
  case 2:
    console.log('either: 1, 2');
    break;
  case 3:
  case 4:
    console.log('either: 3, 4');
    break;

  default:
    console.log('something else');
}

// logs "either: 1, 2"
Enter fullscreen mode Exit fullscreen mode

The output is what we expect, thanks to the break after the first log. We are using the fall-trough just trough cases, not through their code blocks and this is fundamental. If we get rid of the breaks entirely once a condition is met, all possible actions for every subsequent case (even unmatched will fire).

Let's simplify again:

Example 4

switch (2) {
  case 1:
    console.log('1');
  case 2:
    console.log('2');
  case 3:
    console.log('3');
  case 4:
    console.log('3');
}
// outputs 2, 3, 4 because we are switching on 2, if we were switching on 1 it would log 1, 2, 3, 4
Enter fullscreen mode Exit fullscreen mode

Things can get even absurder when we switch on true which is a common pattern to assess a bunch of conditions that need subsequent actions.
Another example from same MDN page:

Example 5

switch (true) {
  case "fetch" in globalThis:
    // Fetch a resource with fetch
    break;
  case "XMLHttpRequest" in globalThis:
    // Fetch a resource with XMLHttpRequest
    break;
  default:
    // Fetch a resource with some custom AJAX logic
    break;
}
Enter fullscreen mode Exit fullscreen mode

This works because fall-trough is prevented and because the two conditions for the two cases are mutually exclusive.

The mechanism of the switch works so that an expression (yes, not just values) is switched onto and every case also presents an expression (again, not just values). The two expressions are strictly compared (using triple equal sign) to check a match.

We could not have a fall-trough implementation when switching on true:

Example 6

switch(true) {
  case false: 
    console.log('Log false');
  case 1: 
    console.log('Log 1');
  case true: 
    console.log('Log true');
}
// logs: Log true (expected)
Enter fullscreen mode Exit fullscreen mode

This code barely stands as the matching case is last...
But what happens if we move the last case in the first position is not so expected:

Example 7

switch(true) {
  case true: 
    console.log('Log true');
  case false: 
    console.log('Log false');
  case 1: 
    console.log('Log 1');
}
// logs: Log true, Log false, Log 1
Enter fullscreen mode Exit fullscreen mode

You can obviously deal with it if you know how it exactly works but this is just a bad API no excuse, and should be avoided.

Let's now rewrite the animals example using a map:

const mapAnimalsExtinct = {
    Cow: false,
    Giraffe: false,
    Dog: false,
    Pig: false,
    Dinosaur: true,
};
console.log(
    mapAnimalsExtinct[animal]
    ? 'This animal is indeed extinct'
    : 'This guy is alive and kicking'
);

Enter fullscreen mode Exit fullscreen mode

The values are now repeated but that's content and much more database-y, so to be expected. Repetition in data has nothing to do with repetition in code as it is declarative and declarative code depicts intents and content.

To minimize it further we could annotate only the extinct animals from the original complete list.

It is a statement, not an expression

  1. Statements do things, expressions are evaluated down to values.
  2. Statements can be placed only in the body of code block, expressions can be place anywhere (inlined anywhere, arguments default values, etc.).
  3. Expressions are referentially transparent and always correspond to their computed value.
  4. Many statements can be replaced by expressions.

The topic is complex and will deserve its own article.

Conclusion

Switch is archaic, verbose, repetitive and poorly designed.
Since most often it is used to map certain values to certain behaviors, we can use maps instead, having a leaner and immediately readable code.

Thanks.

Comments 6 total

  • david duymelinck
    david duymelinckJun 1, 2025

    I think the only use of a switch is when you use return. for example

    function number(nr)
    {
      switch(nr){
        case 1: return 'one';
        case 2: return 'two';
        default: return 'not a number';
      }
    }
    
    Enter fullscreen mode Exit fullscreen mode

    No erroneous fall-through here.

    I would not solve every switch statement with a map.
    The animals example could also be rewritten as

    function isExtinct(animal) {
      if(['cow', 'dog'].includes(animal)) {
         return 'This animal is not extinct.';
      }
    
      if(['dinosaur'].includes(animal) {
        return 'This animal is extinct.'
      }
    
      return 'animal not found';
    }
    
    Enter fullscreen mode Exit fullscreen mode

    This code is more future proof. In case of the map with the boolean, you have to rewrite everything if there is a request to add almost extinct animals.

    • Pierluigi Pesenti
      Pierluigi PesentiJun 2, 2025

      Hey, thanks for the feedback.

      The return acts like the break, preventing the described behavior. Still it is WET as case and return are repeated every time.

      const map={
        1: 'one',
        2: 'two',
      }
      const number = (nr) =>= map[nr] || 'not a number';
      
      Enter fullscreen mode Exit fullscreen mode

      For the animals example there is not much to rewrite if the boolean becomes some other type. Actually it becomes shorter and future proof.

      const state={
          0: 'not extinct',
          1: 'extinct',
          2: 'almost extinct'
      }
      const mapAnimalsExtinct = {
          Cow: 0,
          Giraffe: 0,
          Dog: 0,
          Pig: 0,
          Dinosaur: 1,
          Unicorn: 2,
      };
      console.log(state[mapAnimalsExtinct['Unicorn']]);
      
      Enter fullscreen mode Exit fullscreen mode

      Your animals examples is absolutely valid but consider its data structure is orthogonal to my example (and it requires linear time to access instead of O(1)). If we must stick to that data structure we could dry it up as the logic is repeated N times (would be three times if we add the "almost extinct"):

      This would be yours with the third category:

      function isExtinct(animal) {
        if(['cow', 'dog'].includes(animal)) {
           return 'This animal is not extinct.';
        }
      
        if(['dinosaur'].includes(animal) {
          return 'This animal is extinct.'
        }
      
        if(['unicorn', 'troll'].includes(animal) {
          return 'This animal is almost extinct.'
        }
      
        return 'animal not found';
      }
      
      Enter fullscreen mode Exit fullscreen mode

      It would be DRYer with some map supporting it. Growing the number of cases the code would not grow, only the map.
      The choice of a reducer over a for..of is just arbitrary.

      const animals=[
        [['cow', 'dog'], 'This animal is not extinct.'],
        [['dinosaur'], 'This animal is extinct.'],
        [['unicorn', 'troll'], 'This animal is almost extinct.'],
      ];
      
      const isExtinct=(animal, map) => map.reduce((acc, tuple)=>{
          const [collection, message] = tuple;
          if(collection.includes(animal)) acc = message;
          return acc;
      }, 'animal not found');
      
      console.log(isExtinct('troll', animals));
      
      Enter fullscreen mode Exit fullscreen mode

      I still do not see any compelling case (pun intended) for a switch

      • david duymelinck
        david duymelinckJun 2, 2025

        You are right a map has a better O notation.
        But when the penalty is negligible I rather use early returns. I find the separation between the different choices more readable.
        That is also the reason to use switch with return.

  • Nathan Tarbert
    Nathan TarbertJun 2, 2025

    Honestly, I ditched switch for maps ages ago and never looked back. Cleaner every time.

    • Pierluigi Pesenti
      Pierluigi PesentiJun 3, 2025

      Same here. I actually ditched also 90% of my if statements (I am writing a follow-up article about that).

Add comment