DEV Community

Jonathan Kuhl
Jonathan Kuhl

Posted on

A Case Against Switches

The Problem

Switches are ugly. They are bug prone. The default fallthrough behavior is begging for errors. I think Swift did the right thing and made fallthrough a keyword, rather than a default, but even then, I'd rather avoid them if I can. They just don't fit in with the rest of my code. The indentation is awkward and no one can seem to decide if the case statements are indented or not.

Python didn't even bother to implement them in the language.

I primarily work in JavaScript, so I'll be focusing on that language. However, any language with first class functions and some sort of key/value pair structure can avoid switches. Java, for example, can use maps and lambdas. But I'll be sticking with JavaScript.

The JavaScript Object

How do we avoid switches? Well, each case in a switch is essentially a key-value pair. You're matching a single key, the case, with a single value, an expression to be evaluated, or a set of instructions. Sound familiar? Boil it down to two key works, key and value and you have a basic JavaScript object. How do we use a JavaScript object in place of a switch?

Well let's start with an example. Let's pretend we have some code that displays an error message when a log in fails.

errorMessage(error) {
    const status = {
        '401': 'Please check your username and password',
        '404': 'Account not found, have you registered?',
        '500': 'Something went wrong with the server, please try again later',
        'Failed to fetch': 'Servers are currently down, please try again later'
    }
    return status[error.status];
}

Here we have code that behaves like a switch. We have 4 different error messages that can get thrown, a 401 if verification fails, a 404 if the user isn't found, a 500 if something broke, or a Failed to fetch if the server is down. All we have to do is a very basic look up on an object and that's it. No fall through, no jarring switch structure. Just a basic JavaScript object.

But what if I wanted a default case? Well that's simple too, we just need to check if the value is in the object itself. There are a number of ways to do this, but I'll just check if the property exists by checking for undefined:

errorMessage(error) {
    const status = {
        '401': 'Please check your username and password',
        '404': 'Account not found, have you registered?',
        '500': 'Something went wrong with the server, please try again later',
        'Failed to fetch': 'Servers are currently down, please try again later',
        default: 'Something borked, sorry!'
    }
    if(!status[error.status]) {
        return status['default'];
    }
    return status[error.status];
}

Current JavaScript is also fairly flexible. If I wanted to use numbers rather than strings for object keys, I can do that. JavaScript will, under the hood, turn them into strings. Therefore the following is also valid JavaScript:

const object = {
    1: 'one',
    2: 'two',
    3: 'three'
}

object[1]; // 'one'

Of course, you can't use dot notation on this object, object.1 is invalid, but if we're simply using this object as a switch, then it doesn't matter. With bracket notation, dot notation isn't mandatory anyways. But what's important here is that we can recreate switch behavior with both strings and numbers. Now you could use true and false as strings for keys if you wanted to make a boolean, but I argue a switch is overkill for a boolean anyways.

Functions?

However, when we're using switch, we're often doing more than grabbing strings and numbers, we might also be holding functions. Thankfully, JavaScript is a language that treats functions as first class citizens. Functions can be passed around like any other object, and of course, can be the values of properties in our objects.

Here arrow functions truly shine, though if you need to preserve this, you'll have to reach for Function.prototype.bind(), or use the old school syntax for JavaScript anonymous functions,function () { .... Function shorthand in JavaScript objects also preserve the context of this and in that case, the name of the function, becomes the key, and the instruction block becomes its value.

const greet = {
    sayHello1: function() { return 'Hello' },
    sayHello2() { return 'Hello' },
    sayHello3: ()=> { 'Hello' }
}

In this example, greet.sayHello1() and greet.sayHello2() do exactly the same thing. greet.sayHello3() is slightly different because it is an arrow function and therefore the this keyword is lost. However since the function isn't using this, all three are exactly the same in this particular scenario. If you needed this for an arrow function, you can do greet.sayHello3.bind(greet).

Imagine we have a text based RPG. You play a wizard who has a number of spells he can cast. The user types in the spell he wants and the wizard casts it. You could use a switch to determine which spell to cast, or use an object:

function castSpell(spellname) {
    const spellbook = {
        fireball: ()=> 'Wizard casts Fireball!',
        iceshard: ()=> 'Wizard casts Ice Shard!',
        arcanemissiles: ()=> 'Wizard casts Arcane Missiles',
        polymorph: ()=> 'Wizard casts Polymorph!',
        default: ()=> 'Wizard doesn\'t know that spell.'
    }
    if(!spellbook[spellname]) {
        return spellbook['default']();
    }
    return spellbook[spellname]();
}

So what the function does is, you pass in a spell name, and it uses the spellname to match a value in the spellbook. That value is a function, so by using () after grabbing the value will call that function.

I'm using bracket notation for the default method here for consistency. spellbook.default() would also be valid.

Here we can call functions the same way we would in a switch. You can abstract away all the code that would be your case statements and shove them in an object methods and simply call them via bracket notation.

This does have some trade offs, as it's harder to tell what spellbook[spellname]() is doing than case 'fireball': return fireball(); but the code is more elegant, there are fewer levels of indentation, and no threat of fallthrough.

But I Want Fallthrough!

Oh. Well then. Obtaining fallthrough behavior in an object is more difficult and there's no one way to do it. There could be an argument here where switch might actually be a better construct to use. And if so, then use switch. But understanding that objects have a number methods on them, there are other solutions as well. With Object.values(), Object.keys() and Object.entries(), you can get all of your key/value pairs into arrays and then run them through any number of array functions. This can be done to achieve fallthrough.

Imagine we have an object with a bunch of functions and, given a number, we need to call all the functions upto, and not including that number. This is one case where a switch fallback is useful, but this is also easily done with an object. Here's an example:

function callFns(number) {
    const object = {
        1: ()=> console.log('one'),
        2: ()=> console.log('two'),
        3: ()=> console.log('three'),
        4: ()=> console.log('four'),
        5: ()=> console.log('five')
    }

    Object.keys(object).forEach(key => {
        if(key >= number) {
            object[key]();
        }
    });
}

Call callFns(3) and it will log 'three', 'four', and 'five' to the console. This would simulate using switch(3) { with no break or return in any cases. By combining Object and Array methods, we can simulate a fallthrough for our situation. But again, this may be a case where a switch might be the better construct. After all, the primary cause of bugs in a switch is the fallthrough feature. However, by using an Object, you get access to a number of methods that can make an object more flexible than a switch statement. By getting an array of your object's entries, you get access to filter, reduce, some, every, as well as iteration methods like map and forEach, and structures like for of.

In Summary

Objects in JavaScript give you a simple alternative to the switch statement. Objects are flexible and less bug prone than switch statements and they aren't as jarring in your code as switch statements. If you don't want fallthrough, using an object in place of a switch is a better option. If you do want fallthrough, it can be achieved through Object and Array methods, but a regular switch might be a better option.

All in all, your style of code is up to you, but I suggest being like Python and throwing the switch out entirely.

Happy Coding.

Top comments (16)

Collapse
 
darkain profile image
Vincent Milum Jr

I can understand this from a JavaScript perspective, but this technique is very language specific. One of the HUGE reasons why switch statements exist in the first place is due to the way C/C++ compilers can optimize them behind the scenes. They're significantly faster to process if done correctly, because the compiler will build a jump table rather than a series of comparisons. In modern programming where we're just basically doing glue layers and performance isn't as critical, it doesn't matter much, but when you're working with microcontrollers or doing hardware drivers, it makes a world of difference.

Another advantage of switch statements is that some languages will allow for backwards comparisons. You can do something like "switch (true)" and then run a ton of cases to see which one is true first, and then process accordingly.

Collapse
 
jckuhl profile image
Jonathan Kuhl

Sure, if the language you're working in has a lot of optimization for switches, then go with what works best for the language. My post was indeed mostly centered around JS. I know little about C++, so I won't presume to speak on it. If switches are better in C/C++, stick with them.

And even in JavaScript, if the situation is such that a switch is the simplest and most efficient way to achieve what you're looking for, then I'd remain with the structure.

Collapse
 
codevault profile image
Sergiu Mureşan

Switches, as you said, are an ugly and poorly implemented concept. This object to switch trick is fairly useful in cases you need lots of cases to check for.

Call me crazy but, most of the time, I simply use a long else if chain when needing a switch. It feels natural, it's much more readable than the object one and it works in any language.

All in all, very useful post!

Collapse
 
darkain profile image
Vincent Milum Jr

Some language implementations have a limit to the number of "else if" statements you're allowed to use though.

Collapse
 
codevault profile image
Sergiu Mureşan

Interesting. I never knew that. What languages have this limitation?

Thread Thread
 
darkain profile image
Vincent Milum Jr

It isn't language specific, it is implemention specific. I know some C/C++ compilers have this issue, but not all.

Thread Thread
 
codevault profile image
Sergiu Mureşan

For embedded I can see that limitation and that switch could look much more readable since you're mostly working with numbers (integers, addresses etc.) instead of more complex structures (objects, strings etc.)

Collapse
 
aodev profile image
AoDev • Edited

With your example it works because values are non empty strings.
But since you are explaining a pattern here, it must be said that the check for default value is actually buggy.

Any falsy value will lead to the default "branch" because using "if" like this will check the value at key "x" and not if the key exists, unlike switch that actually matches against case.

You should be checking if the key is defined instead.
An example could be:

return choices.hasOwnProperty(x) ? choices[x] : choices.default
Collapse
 
jckuhl profile image
Jonathan Kuhl

That's a good point.

Collapse
 
miguelpinero profile image
Miguel Piñero • Edited

Maybe you can do something like this:

return choices[x] || choices.default
Collapse
 
aodev profile image
AoDev

This would not work. I have explained why above.

Collapse
 
joelnet profile image
JavaScript Joel

I totally agree. Another cool option is to use Ramda's cond.

var fn = R.cond([
  [R.equals(0),   R.always('water freezes at 0°C')],
  [R.equals(100), R.always('water boils at 100°C')],
  [R.T,           temp => 'nothing special happens at ' + temp + '°C']
]);
fn(0); //=> 'water freezes at 0°C'
fn(50); //=> 'nothing special happens at 50°C'
fn(100); //=> 'water boils at 100°C'
Collapse
 
erebos-manannan profile image
Erebos Manannán

If you want something like that, it really seems much less "magical" to simply use a class + methods.

Even in old JS this seems clearer:

function spellbook() {
}

spellbook.prototype = {
  fireball: function() {
    return 'Wizard casts Fireball!'
  },
  iceshard: function() {
    return 'Wizard casts Ice Shard!'
  },
  arcanemissiles: function() {
    return 'Wizard casts Arcane Missiles'
  },
  polymorph: function() {
    return 'Wizard casts Polymorph!'
  },
  default: function() {
    return 'Wizard doesn\'t know that spell.'
  }
}

var spells = new spellbook()
var spell = 'fireball'
if (!spells[spell]) {
  spell = "default"
}
console.log(spells[spell]())
Collapse
 
g4vroche profile image
Hugues Charleux • Edited

An other point in favor of objects instead of switches, is that they are composables: we can spread the declaration of a big object through multiple files for a better code organisation.
It's a great pattern for example to keep Redux reducers gathered by domain / feature

Collapse
 
martinhaeusler profile image
Martin Häusler

You're barking up the wrong tree here. In day-to-day JavaScript practice (at least in my experience) there is very little actual object orientation involved. The frameworks do the OO stuff, "client" code is mostly object literals and JSON. Hardly anything has a prototype other than "object". And switch statements are all over the place. I don't agree with this style of programming, I'm with you on replacing switches with polymorphism. But I'm afraid the JS world won't listen.

Collapse
 
zayelion profile image
Jamezs Gladney

Good idea, I love purity, but when looking for a bug with a debugger it doesnt stop on the code and does not make the error very clear. So I keep cases in and put demand for break in the linter.