Hacker News new | past | comments | ask | show | jobs | submit login
Using the switch(true) pattern in JavaScript (seanbarry.dev)
120 points by seanbarry 9 months ago | hide | past | favorite | 145 comments



I prefer early return pattern.

Which would look like this:

    const user = {
        firstName: "Seán",
        lastName: "Barry",
        email: "my.address@email.com",
        number: "00447123456789",
    };

    if (!user) {
        throw new Error("User must be defined.");
    }

    if (!user.firstName) {
        throw new Error("User's first name must be defined");
    }

    if (typeof user.firstName !== "string") {
        throw new Error("User's first name must be a string");
    }

    return user;


The early return pattern was the most effective single piece of advice I received from a senior dev on how to make my code more readable. You end up with clearer code paths than any other pattern I have seen so far and way less indentation making things look (and probably perceived) as less complex.

Pair it with naming your complex and chained expressions and suddenly you have some seriously readable code.

So far, I have never seen a valid scenario where a switch statement is actually any better than if.


I think the early return pattern makes the most sense when it's less "logic" and more "validation." Eg in the example, the code isn't really _doing_ anything if there's no user or no first name, it's running a bunch of checks _before_ trying to do anything. Those validation checks are implemented with if/early return, but the actual pattern is more "validate first and only proceed if everything checks out."

In typed languages a lot of those checks get implemented in your actual types, but you still might have various business logic/data integrity checks you might implement in early return.

Seen in this light, this pattern's really not so much in tension with the idea of having a single return variable. It's just a way to implement the idea that invalid states should not be possible, which you accomplish in your type system when and if possible, and fall back to runtime checks for the gaps where it's not.


Early return can get into trouble if you're talking about a function that has a fair bit of state to unwind in the event of an error. Open filehandles, allocated memory, general cleanup stuff.

That said, I much prefer early return whenever it makes sense. In functions that do have a lot to unwind and many possible points of failure I'll pull out the old villain 'goto' and have the unwind code at the bottom of the function.

Strictly sticking to only one approach is usually a mistake. One that is repeated a lot in computer science. There are schools of thought that if everything is the same it will be easier to understand, but what happens is problems that don't exactly fit the mold end up being solved in awkward and inefficient ways. Or development gets slowed because you have to refactor your problem around the tools instead of the other way around.


> Early return can get into trouble if you're talking about a function that has a fair bit of state to unwind in the event of an error. Open filehandles, allocated memory, general cleanup stuff.

This is where Lisp's unwind-protect, finally blocks in some languages, defer in Go, and C++'s RAII pattern can come in handy. Especially if you have a healthy respect for goto and want to minimize its presence in your code for various reasons.


`finally` introduces additional blocks, so if you have 5 different resources that you acquire as your function goes along, you end up with 5 levels of curly braces. Not very elegant.

`defer`, on the other hand, is probably the only thing I wanted to take from Go to other languages I worked with. Beautifully explicit and wonderfully useful.


C#’s new syntax for `using` also helps:

Old:

    using (var file = OpenFile())
    {
        // use file
    }
New:

    using var file = OpenFile();
    // file will be disposed when it goes out of scope


> `finally` introduces additional blocks

It would be cool if someone made a way to scope variables at the function level in JavaScript instead of at the block level. I might write a transpiler for it…


I've never considered writing a proposal for a language feature before, but after 2 minutes of googling, this actually looks completely doable:

https://github.com/tc39/ecma262/blob/master/CONTRIBUTING.md

So, impostor syndrome aside, why not? Wanna team up on that?


whoosh.gif


Okay, it was 2am here and I must have misread your comment and lost the 'variables' part.


It is already possible: var makes a variable function-scoped, let/const makes a variable block-scoped. Or are you referring to something else?


I don't know of an equivalent in the native API, but Bluebird's "disposer" implements this in the context of promises: http://bluebirdjs.com/docs/api/disposer.html

It would be nice if there were an equivalent in the native API. You can use .finally() with chained promises, but as far as I know there's nothing comparable with async/await yet, and that's a much more comfortable syntax with which to work with promises overall.


Unpopular opinion: I think early-returns make code less readable if you don't put the rest of the function in an else clause. You lose visual parallelism, and suddenly instead of following a tree down to a series of leaves where every leaf terminates, you have to have the full context to know whether or not a line of code may not execute in some cases. For example:

  function foo(x) {
    if (x == null) {
      return null;
    }

    x += 2;
    return x;
  }
I can't just look at "x += 2;" and know whether or not it's conditionalized. I have to have the full context including the early-return, and then reason about the control flow from there. Whereas:

  function foo(x) {
    if (x == null) {
      return null;
    } else {
      x += 2;
      return x;
    }
  }
Here I can tell just from the else-block that this is one possibility which will execute if the other one does not, and vice-versa. Their indentation is the same, cementing their relationship. If I want to know whether this block will execute I need only look at the if()'s, not their contents.


I think your version isn't really as different as it seems to some. To me, the essential bit of early return is that you handle cases that eliminate the work first. That is, it's less about singular if statements separating the rest of the body of the function than it is clearly short circuiting the code as early as is clearly possible.

That is, I don't think it matters too much whether you do:

    function div(x,y) {
      if (y == null) {
        return null;
      }
      if (y == 0) {
        return null;
      }
    
      return x/y;
    }
or

    function div(x,y) {
      if (y == null) {
        return null;
      } else if (y == 0) {
        return null;
      } else {
        return x/y;
      }
    }
as they both accomplish the same major benefit of the pattern.

The real thing it's helping to avoid is accidentally creating something like this:

    function div(x,y) {
      if (y != null) {
        if (y != 0) {
          return x/y;
        } else {
          return null;
        }
      } else {
        return null;
      }
    }
which can quickly get very hard to reason about without in more complex cases.


You're right that both technically qualify as early-return, which is why I qualified my original statement with "if you don't put the rest of the function in an else clause".

In practice, in C-like languages, I tend to see the "else-less" version which is why I brought it up


If you haven't seen it, Kevlin Henney's talk about "The Forgotten Art of Structured Programming"[1] is interesting (if somewhat long in the examples later), and somewhat related. It's not specifically about return early (and a case could be made that it should or should not apply specifically to early return), but it is about if, else, return, and how they affect cognitive load when reading code.

1: https://youtu.be/SFv8Wm2HdNM


On the contrary, if early returns allow you to outdent a larger portion of code then it's quite a readability win. As long as all the early return cases are bite-sized and the last case long-ish, it's a readbility win.


And certain languages allow:

return nil if n.nil?


I really miss postfix conditionals from perl/ruby/coffeescript in all of the other languages. (Along with until/unless, which are just while! and if!.)

I asked on the go-nuts mailing list about perhaps including them in go2, but it seems many of the people who replied hate them.


I have the same opinion. I like the early-return pattern when it's doing input validation or raising errors but for most other things I prefer how the if-else highlights that there are two possible paths that the code can take. Just because the "then" branch has a return statement doesn't mean that we should get rid of the "else".


I think the bigger problem you may be experiencing, if I had to guess, code structure.

If people tend to write:

   function foo(x) {
     if (x == null) {
       return null;
     }

     // do some stuff to the code


     for(a reason) {
        // do some more stuff
        if(some detailed reason) {
          return null;
     }

     // more things
     return 7;
   }
Then the shape of the code and the "pattern" of early returns gets broken.

If you read code where all of the short-circuit, early-return logic is at the start of the function, and then all of the work is done, do you still have this opinion?

E.g.

   function foo(x) {
      if(x == 1) { return null; }
      if(x == 2) { return null; }
      if(x > 7) { return null; }

      // do things with x
      return 7;
   }
?


The problem is definitely much worse when returns are scattered throughout the function instead of grouped at the top, but I still prefer them to be "elsed" together (or in a switch/cond/match statement or whatever). Worth noting that in Lisp - one of the classical languages where early-returning is popular - there isn't a problem because it is done as I describe:

  (cond
    (early-return-condition early-value)
    (second-early-condition early-value2)
    ...
    (t final-else-value))
The "default case" is on the same footing as the other cases. Only in procedural languages does the control-flow get confusing if you aren't careful.


If you want elses everywhere, how do you protect yourself from nesting hell? I find nesting hell to be far, far, faaaar worse for readability than any of the alternatives, and will move to the early return model to escape all that nesting.


I dunno, I just rarely ever find myself nesting more than two or three levels of conditionals even in extreme cases. If my logic really is that complicated, it's probably a sign it needs to be broken up anyway. And if I did need that much branching, I would want to be more explicit about it, not less.

Also - at the very least - you can mimick the "flat" early-return style and just stick some else's in there and call it a day. That's my main point; less so the actual deeper nesting


I agree, but also I think this is misrepresenting the 'early return' pattern. It's more likely to be succinct and look like this instead:

    function check(x) {
        if (!test1 (x))   return false;
        if (!test2 (x))   return false;
        if (!test3 (x))   return false;
        if (!test4 (x))   return false;
        return true;
    }
In _this_ scenario, it is true that this is much more readable than:

    function check(x) {
        if (!test1 (x)) {
           return false; 
        } else if (!test2 (x)) {
           return false;
        } else if (!test3 (x)) {
           return false;
        } else if (!test4 (x)) {
           return false;
        } else {
           return true;
        }
    }


Never ever use if without curly braces. That's one of the worst ideas of C's syntax.

Your first example would still look clean like this:

  function check(x) {
      if (!test1 (x)) { return false; }
      if (!test2 (x)) { return false; }
      if (!test3 (x)) { return false; }
      if (!test4 (x)) { return false; }
      return true;
  }
And it would be much safer from stupid copy/paste errors.


Instead of

    function check(x) {
        if (!test1 (x))   return false;
        if (!test2 (x))   return false;
        if (!test3 (x))   return false;
        if (!test4 (x))   return false;
        return true;
    }
why wouldn't you do something along the lines of

    (every (lambda (f) (funcall f x)) (list #'test1 #'test2 #'test3 #'test4a))
or whatever is the equivalent in your preferred language?


The tests are rarely that simple or regular.


Isn't that exactly the reason to put them somewhere else?


No, I just mean the actual tests might be something like:

    if (user == null) return false;
    if (IsCompleted(user)) return true;
    if (action == null) return false;
    if (value < 0 || value > 100) return false;   
You're not usually just passing a single value to a bunch of "test" functions.


Well, the way it was written, I was assuming that conditions to be satisfied had separate named predicates (although combinators could alleviate even some of these things).


Worth noting that elses don't require curly braces, which are where most of the visual noise is coming from in your example:

  function check(x) {
    if (!test1 (x))      return false; 
    else if (!test2 (x)) return false;
    else if (!test3 (x)) return false;
    else if (!test4 (x)) return false;
    else                 return true;
  }
I think this is more clear than either of the above


> Worth noting that elses don't require curly braces

True. I still think the first is clearer though. Either all tests pass, or they don't.


(also I'd be very skeptical of such an if..else ladder. Prime target for bugs.)


That code is ugly anyway, something I would have wrote in middle school... why not:

    function check(x) {
        return test1(x) && test2(x) && test3(x) && test4(x);
    }
Or, since I assume is JavaScript, just:

    const check = x => test1(x) && test2(x) && test3(x) && test4(x);


Nobody would actually write this, it's just a demonstration of the pattern at hand for the sake of discussion


For a single condition it doesn't matter much either way for readability. But once you have a number of conditions the if/else style becomes unwieldy.

I put all my early returns at the top as these are the preconditions for the rest of the code; I don't think that's hard to understand.


Nit, the x is a copy so it won't change the value of x outside the function. So I would rewrite ad

const foo = x => x !== null ? x + 2 : null


Sadly, a lot of junior and medior programmers see that as a wrong pattern, and opt for a return variable.. Drives me nuts.


I recall this pattern being taught in my first year or two of my computer science degree. Emphasizing only one return statement per function, utilizing a shared return variable.

I much preferred (and still greatly prefer in industry) the pattern of exiting the function as soon as possible.


It's like "goto considered harmless" -- structured code to max, to the point of unreadability. Dogmatism has drawbacks.


Same here.

Returning and throwing quickly and strict.


In C when you have nothing that needs cleanup, it's not the most horrible thing ever:

  int foo(thing_t \*work) {
    int ret = E_SUCCESS;

    if(work->thing == BAD_THING) {
      ret = E_BAD;
      goto exit;
    }
  
    do_some(work);
  
    if(work->thing2 == OTHER_BAD_THING) {
      LOG("whoopsies");
      ret = E_OTHER;
      goto exit;
    }
  
    ret = do_more(work);
    if (!ret) {
      LOG("lazy");
      goto exit;
    }
  
    ret = last_bit_of(work);
  
  exit:
    return ret;
  }


I find the safe way to program that is always to set the initial value to some error. You only set it to success at the point you succeed. Then you never have to worry about some odd goto or other break in the control glow being introduced by you or someone else that wasn't paying attention. It's also better self documentation, because then it's obvious at what point you're actually done, in the case your final work isn't aptly named "last_bit_of_work(work)", because you'll see the return variable set to E_SUCCESS immediately afterwards.


agreed, but some people dislike that because you have to have an E_UKNOWN or E_GENERAL or something like that in addition to more specific errors and others dislike it because now there's not something to conveniently store results from called functions in so that they can be checked for errors.


I see that as a sign that the person learned how to program in the 80s/90s when things were still in a phase of overreaction to the popularity of BASIC and GOTO. GOTO is bad, therefore return is good, and single return is best. Not actually good logic, but that was the thinking at the time.


They should really use both


Unwanted conditions should be handled before handling the process in a peaceful condition.

Not sure it makes much sense to indent everything within "if" and if you forget to "else", you've just potentially hidden a bug.


I haven't written else if in javascript in years, and else is a very very rare staple in my code. Return pattern was an eye opener from a senior dev.


Agreed! I have three rules for writing functions:

- Return early

- Return often

- Reduce levels of nesting

https://erock.io/2019/07/11/three-rules-for-refactoring-func...


Early return is my silent gauge to tell if a piece of code has been written by a junior or a senior software engineer.

I still see far to many snippets with multiple levels of indentation, where each if branch is for the happy path, and if they were converted to early returns you could flatten the whole thing to 1 indentation level, at the expense of requiring negated boolean expressions which aren't as readable.


No need to ‘return’ when you throw an error, but your approach is valid IMO: if structure / readability is that important, refactor the switch to it’s own method with only if checks in it. Hard to make that simpler and more readable.


If you don't have a 'return' there's nothing to say that some time later some junior developer would not modify throw to be something else or forget that throw does not return.


This will prevent the JR dev from making that mistake: https://eslint.org/docs/rules/no-fallthrough


That's what tests are for.


So that has got a name? I always instinctively wrote code like that, because it's much more easier to understand.


I've grown to dislike the early return pattern. It's pitched as a way to reduce visual complexity by reducing levels of indentation, but I don't actually find that to be a benefit in most cases. Reducing indentation is just a trick to shove more cyclomatic complexity into a function without it triggering your sensibilities to implement proper abstractions.


I've also seen code written as:

  _ = !isDefined(user) && throw new Error("User must be defined.");  
  _ = !isString(user.firstName) && throw new Error("User's first name must be a string");
But I while it is concise, I can also understand why people prefer regular if statements.

Doesn't seem to be valid JS, can't remember where I got it from


In my opinion this pattern is better if you write it like this:

  _ = isDefined(user) || throw new Error("user must be defined")
This reads way more natural for me. "A user is defined OR throw an error"...

I've also seen this in Perl (`do_something() || die()`) and shell scripts (`grep -q || die "not found"`).


In perl you would want to use `or` instead of `||` to take advantage of the low precedence, so you can type things like `dostuff $foo or die` which is logically 'do stuff, and if it fails, die' as opposed to 'die if $foo is false', which you'd get with ||


It's a stage 2 proposal, which technically can be used today via babel[0] (though personally, I don't recommend using stuff below stage 4)

[0] https://babeljs.io/docs/en/babel-plugin-proposal-throw-expre...


My preference for this is to use an assertion function, eg in nodejs:

assert(isDefined(user),”User must be defined”) // throws if Falsy


This pattern is quite common in React to do conditional rendering:

  {isLoading && <Loading />}


switch true is a cool new trick to me, but early return is the way to go.


Linting got messed up in the comment above, but in the actual code this is nicely readable.


I agree; `if (` is much more well-known and "grokkable" to engineers at practically any level than `case` - and exactly the same number of characters. There's no need to bring a sledgehammer when the nail is perfectly handled by a normal hammer.


I agree, but the problem presumably is the “else”.


switch true is not a substitute for early return it is a replacement for the equivalent written in if chain


1000X this.


I would usually agree, but the suggested code would allow for multiple errors to be shown before they’re thrown.


I think the issue here though, is that for certain cases such as form field validation, you want all issues to be returned at once. Using the switch method or similar message packages allow you to inform your users that have N issues with a page in 1 request as opposed to N.


That’s not how throw works. And that’s exactly why the switch(true) pattern should not be used: novice users not fully knowledgeable about every specification of a language should not fail to understand such a basic piece of code.

There’s not even a line difference to using if statements correctly (as others in the comments have demonstrated) The one thing an if statement does is checking if something is ‚true‘. I don’t understand why anyone would use a ‚switch‘ here apart from showing how clever they are.


Either you will break, thus exiting the switch-case block or you will fall-through. The fall-through behavior won't do what you want:

> the script will run from the case where the criterion is met and will run the cases after that regardless if a criterion was met.

Using the switch method won't allow you to return several errors while the simple ifs method described earlier could accumulate the errors and return later. The switch method is elegant though.


A good example of: just because you can, it doesn’t mean you should. Yes, the if/else sequence is a bit more unstructured in terms of code layout, but chances the next developer browsing the code will instantly know what it does will be much higher.


Spare a thought for the junior/mid-level devs who don't understand clever patterns, and the seniors who haven't read the same blog posts as the implementor of the switch(true).

Completely agree, it's like writing readable prose, understand your audience.

That said, give me proper pattern matching in JS. Hard to live with languages that haven't caught up yet.


> That said, give me proper pattern matching in JS. Hard to live with languages that haven't caught up yet.

I really want this. Along with switch being as expression. Seems like something that JavaScript is obviously missing.


> Along with switch being as expression

Even PHP has this now, albeit with a different keyword (match) due to existing switch semantics being pretty bad


I really want... switch being as expression.

Unfortunately JavaScript still hasn't caught up with CoffeeScript.


Of course, you can't evaluate prose in a REPL and see for yourself what it does.

I agree that overly clever code is always to be avoided. But is this really overly clever? I wouldn't expect an engineer early in their career necessarily to understand on sight what it does. But I might worry a little for one who couldn't figure it out through experiment.


I consider myself an advanced-beginner programmer untested in the real world (looking for my first coding job though!), and I understood it relatively quickly. It does appear more concise than many if-else statements. After the initial “what?”, I was able to quickly parse through it. I think this is faster than using the standard form.

[edit: fixed typo]


A lot of the code you're going to have to be dealing with in the wild is going to look fine, until it breaks at 2am on a Saturday and you're 3 drinks in and production is down.

You're going to hate your former cleverness then.


There should be a rule that forbids applying this reasoning to code that isn't triple-nested template SFINAE mess, or a code-generating-code-generating-code-generating-code Lisp macro.

This pattern isn't "cleverness", it's just a readability-improving trick that's cheap to figure out. The hundred seconds you spend on it seeing it for the first time is a one-time cost.


That's why I abstain when I'm oncall (or backup).

That's what the oncall bonus pay is for.


Replace alcohol with 2 hours of sleep, then :)


"faster" in which respect?


Faster to parse as a developer. I can't speak to whether computationally it is faster.

Additionally, doesn't this somewhat also bring in the methodology of dealing with the error cases first?


The only problem here is that you have to do this in the first place. This problem has been solved some 60 years ago with Lisp and `cond` construct, which is even older than `if/else if/else`! Alas, our industry likes to forget history.

That said, this isn't some kind of high-level hackery. If it's cleaner than if/else if chain, you should use it.

Programming is a profession. A professional is expected to learn and grow. The right answer to seeing a code construct one doesn't understand isn't throwing hands up in the air, but spending the few minutes necessary to figure out what it does.


I’d flag this if it came up in PR. It’s clever and novel which is precisely why it’s a bad idea. Always bet on boring.


Same. I also flag seemingly good things like !!x. If you want to convert something to boolean be explicit and do Boolean(x). In most cases it turns out there was a bug in input parameter in the first place and developer was just lazy to fix it.


Exactly, because it won't be the first innovation nor the last and if you let them all through, your code becomes a bazaar of curiosities rather than a business solution...


I think the post doesn't give a fair comparison as in the if/else case there's no need to use "else" blocks if you're throwing an error or returning. In this case I think simple "if" statements are cleaner and certainly more "normal".

E.g.

  if (!user) {
    throw new Error("User must be defined.");
  } 

  if (!user.firstName) {
    throw new Error("User's first name must be defined");
  }

  return user;


When validation gets complex (e.g. there are many criteria to check), I like to build a list/stream/array (what ever the language offers) of tuples of predicates (functions from the object that gets validated to boolean) and strings (or functions from the object to string so I can have context in my error messages).

Then iterate over the tuples, if a predicate fails, return the associated error message and throw an error/display the message to the user.

In the end it looks something like this:

  var validators = Stream.of(
    Map.entry(user -> user != null, "User must be defined"),
    Map.entry(user -> user.firstName != null, "Missing first name"))

  validators.filter(e -> e.getKey().apply(userToBeValidated)).map(Map.Entry::getValue).getFirst()
(This example uses Map.entry for tuples as Java lacks native support for tuples)

This limits branching and you have all validation criteria neatly organized in the same location.


Sure, if you're validating some data there're loads of better ways to do it than a bunch of conditionals. I was purely commenting on the switch(true) pattern vs some "if"s.

That approach looks nice though. On that subject, JS has some nice libraries including io-ts[1] which has a functional approach using Eithers to encapsulate errors/success.

[1]: https://github.com/gcanti/io-ts


Sure, if you have an either/result type, the whole thing becomes a fold, where each validator is a function from user to Either<Error, User> and then

  validators.reduce(Either.right(user), (acc, next) -> acc.flatMapRight(next))
This way you'll end up with either the validated user, or the first error that occurred and all the other validators were skipped.


If you are using a language with yield you can make this simpler (and more flexible) by using a generator.


Agreed, +1 This pattern goes beyond JS and is very helpful for supporting tests; you've already factored out your validators. There's always going to be stragglers but you can define and push in an extra check on the spot (i.e. using lambda funcs at least in JS).


I'm definitely in the minority here, but I'd rather see the `else` block, since it's more explicit at-a-glance, and the logic has more symmetry with all outcomes on the same level.

One reason I like RustLang is it treats this as an ergonomic issue by appending `?` for early returns, without block-nesting. So nice.


Is this a real thing? It looks incredibly hacky to me. What happens when multiple cases are true, are they all handled? In what order? What happens if one of them returns? Etc.


The same way switch works in every language and regardless of the clever pattern: find the first expression that equals and jump to that label.


Many languages don't allow duplicate values for case in switch statements, so that question never comes up - this is true for C, C++, C#, Java. Go, Ruby do behave like JS here.


A better question is: which "case" expressions are evaluated - all of them, or only the ones enumerated before the match was found?

Unless you use this pattern regularly - and most coders don't, even those who mostly write JS - you'll probably have to look the answer up. That alone is a good reason to stick to if/else; there's no such ambiguity there.


The other ambiguity that I questioned right away was whether the language itself guarantees the order or evaluation or if it's compiler dependent? If I switch between Chrome & Firefox would I get different results?

Also, by using non const expressions in case statements you lose the ability to have a large number of cases get mapped into a jump table. Although, I don't even know if more modern languages even support this optimization anymore since I think supporting non const case expressions is becoming the norm in a lot of languages.


JS is specced up the wazoo, including the exact execution semantics. It's fairly difficult to write JS that executes differently between Chrome & Firefox. The only things with intentional nondeterminism in the core language are WeakRef and FinalizationRegistry. And those won't just behave differently between browsers, they may behave differently across browser versions, loads, user activity, etc.

Date parsing has different edge cases between browsers.

The DOM obviously provides more nondeterminism as well as deterministically different behavior, and there are some different things exposed to web pages so it's not like you can't detect which engine you're running on. But things like exact switch case execution ordering are well-covered by the spec.


> A better question is: which "case" expressions are evaluated - all of them, or only the ones enumerated before the match was found?

Good point. By the way the answer is the latter, so the following prints 1 and 2.

    switch (true) {
        case (console.log(1), false):
        case (console.log(2), true):
        case (console.log(3), false):
            break;
    }


Easy enough to remember because this is the simplest thing the language can do, but yeah, I have no idea if this is the same between popular languages.


In the non-Boolean case, a common implementation technique for a large switch with sequential values is a jump table. But that's only possible when all case labels are known.


Good point, using non-pure functions would be confusing.


But in many languages, the cases need to be compile-time constant rvalues.


Hopefully we will get real pattern-matching at some point (https://github.com/tc39/proposal-pattern-matching), but I kinda sorta like this!


Guaranteed to always occur:

   ...
   case !isValidPhoneNumber(user.email):
   ...
Tho see [1] and [2]

[1]: https://github.com/kdeldycke/awesome-falsehood#emails

[2]: https://github.com/kdeldycke/awesome-falsehood#phone-numbers


What a great set of resources. Thank you for sharing this repo!


Careless typo - thanks for flagging ;)


Validation code as shown tends to be repetitive and imperfectly implemented. I have found that transitioning to using AJV and JSON Schema is far more sustainable, especially on an API surface. One describes the data and depends on consistent and vetted logic for validating the described types rather than repetitively describing how to validate them.

Validations that happened at an application level must still be written but those tend to be specific to the application logic or system state. An example of logic related validation is contingently valid argument values where the compatibility of one value being used with another must be tested. An example of state related validation is a constraint that a given value must exist in a dynamic table.


This style gets the label 'poor-mans-pattern-matching' from me. If pattern matching would not be in my daily vocabulary, as it's also not available in JS, I'd consider it a misuse of switch/case and this post also makes an odd example for its usefulness.

The example I would pick is the following: Consider you need to switch depending on a version (of a specification in my case), but this version isn't represented as an enum in the codebase, but as a number instead. So our team had something like this in the codebase (early return):

  function foobar(version: number): string {
    if (version === 3.1 || version === 3) {
      return 'result_3';
    }

    if (version < 2 && version >= 1) {
      return 'result_1';
    }
  
    if (version >= 2) {
      return 'result_2';
    }
  
    throw new Error(`Cannot interpret version '${version}'`);
  }
I read it as "people don't care about branching order that much, so how can I make my wish for better readability more clear?".... my end goal then was to bring it into this state (a distinct enum as discrete value of the version):

  enum Version {
    _1_1 = 1.1,
    _2   = 2,
    _3   = 3,
    _3_1 = 3.1,
  };

  function foobar(version: Version): string {
    switch (version) {
      case Version._3_1:
      case Version._3:
        return 'result_3';
      case Version._2:
        return 'result_2';
      case Version._1_1:
        return 'result_1';
      default:
        (function (val: never): never {
          throw new Error(`Exhaustiveness reached: ${val}`);
        })(version);
    }
  }
...and my interim solution that made it into the PR in time turned out to be something like this (switch true):

  function foobar(version: number): string {
    switch (true) {
      case version >= 3:
        return 'result_3';
      case version >= 2:
        return 'result_2';
      case version >= 1:
        return 'result_1';
      default:
        throw new Error(`Cannot interpret version '${version}'`);
    }
  }
My PR was flagged by the team for misuse of the switch statement, we had some discussion and I changed it back to the simple if/else branching from above.


    switch (Math.floor(version)) {
    case 1:
        return 'result_1';
    case 2:
        return 'result_2';
    case 3:
        return 'result_3';
    default:
        throw new Error('...');
    }
Isn't that more clear?


I think I didn't make my point quite clear. Let's consider the case where version 3.1 needs to return something different than version 3.

A good (IMHO) early-return pattern would look like this:

  if (version >= 3.1) {
    return "result_3_1";
  }

  if (version >= 3) {
    return "result_3";
  }
  
  // ...
...but in the wild I often see it developed into something like this:

  if (version >= 3) {
    if (version === 3.1) {
      return "result_3_1";
    }
    return "result_3";
  }
  
  // ...
...probably because people like to see major version numbers packaged together in blocks.

With the switch-true on the other hand I could make good use of this 'stepping through ranges'-problem and could even make use of the fallthrough, in case it should fall back to a different value:

  switch (true) {
    case version >= 3.1:
       return "result_3_1"; // just comment this out if you need the result of version 3 instead
    case version >= 3:
      return "result_3";
    // ...
  }


Why not just:

    function foobar(version: number): string {
      if      (version >= 3) { return 'result_3'; }
      else if (version >= 2) { return 'result_2'; }
      else if (version >= 1) { return 'result_1'; }
      else {
        throw new Error(`Cannot interpret version '${version}'`);
      }
    }
?

I don't see what advantage the switch provides here.


Good lord this is awful. If somebody's paying you to solve problems with code, please just write clear code, rather than showing off. Somebody's going to have to make sense of it a year from now when requirements change, and you will be in a sense talking to that future programmer (maybe it's you) via code. You should be trying to tell them about the problem, rather than about yourself.


In addition to an already mentioned early return pattern, I also often recommend switching from switch to pattern matching via an object.

It has an additional benefit of extracting code into data structures or making them parametric

  function getArrow(direction) {
    switch (direction) {
      case "left":
        return "<--"
      case "righ":
        return "-->"
      default:
        return "¯\\_(ツ)_/¯"
    }
  }

  function getArrow(direction) {
    const arrows = {
      left: "<--",
      right: "-->",
      default: "¯\\_(ツ)_/¯",
    }
    let arrow = arrows[direction]

    if (arrow) {
      return arrow
    } else {
      return arrow.default
    }
  }


I use this pattern frequently, with minor changes:

  function getArrow(direction) {
    const missing = '¯\\_(ツ)_/¯'
    const arrows = {
      left: '<--',
      right: '-->',
    }
    return arrows[direction] || missing
  }


Yes, I use it exactly like that.


On a similar topic, I'm wondering how often people are using the "else" part of if/else these days. I haven't written "else" in years and I've become very fond of that "if only" pattern.


If you have a chain of conditions what you do?

    if (condition1) {
        // something
    } 
    if (!condition1 && condition2) {
        // other stuff
    } 
    if (!condition1 || !condition2) {
       // finally
    }
An if-else is more clear:

    if (condition1) {
       // something
    } else if (condition2) {
       // other stuff
    } else {
       // finally
    }
To me if-else is more easy to reason about (since it's clear that you enter in one of the 3 possible branches without even looking at the conditions), but also it's more efficient, especially if the condition is not a trivial comparison (for example you are comparing strings, or doing some other linear operation. And yes, computer are fast these days, but there are no excuse for wasting resources for nothing to me).


If that if/else chain is returning a value, then what's even more clear is:

  if (condition1) {
     return something;
  } 
  if (condition2) {
     return otherStuff;
  }
  return finally;
I don't remember the last time I wrote an if/else chain that wasn't in return position.


I agree that it is frequently a "code smell" that indicates a function should be refactored into smaller units.


Often enough, and I use if you capture paths that should never happen logically.


I think the beauty of the switch statement is that when you see one you know you're just concentrating on the value of one variable. I think the if else if is actually cleaner for the user example in the post.


That's good and dandy until one changes one case block to normal statements instead of a terminating one, forgets to add a "break;" and someone has a nightmare debugging session trying to figure what is going on.

Go did good by making case blocks break automatically and requiring the "fallthrough" keyword in one of those very rare cases you need it do.


The article is misleading, in implying that `switch(true)` is a special case: "The fundamental principle of the switch true pattern is that you can match against expressions as well as values."

It should be states as "The fundamental principle of the switch pattern in JavaScript is that you can match against expressions as well as values."

From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...:

A switch statement first evaluates its expression. It then looks for the first case clause whose expression evaluates to the same value as the result of the input expression


For many years I stopped using this after getting flack in pull requests.

I’ve recently added it back into my toolkit and am reminded how much I love it. Don’t over use it but there are some really gnarly if/else blocks that can be expressed beautifully with a switch and fall-thru.


If (x) throw y;

If (s) throw t;

Throw default;


Nope. Not the same. That is a switch with a break.


The example in the post uses return and throw. Break doesn't matter in that case


I am not the author of the post. My comment specifically mentions fall thru.


Even accepting the dubious premise that "pretty text = maintainable code", he's juked his exampled by (i) littering the simple early-outs with unnecessary "else"s and (ii) stripping the "break"s from his switch.


I remember when I first discovered the concept of pattern matching, I tried to find a way to "hack" the switch statement in js to make it work like pattern matching.

The switch(true) format was the closest I got to it, which I personally don't like compared to a clean if/else or early return.

There's probably some performance differences between if/else and switch (I haven't checked) but it probably end up being what's your preference / what standard code style you want to enforce on your team.


Reminds me of a C++ pattern some guy I worked with years ago used to love to simplify complex if checks - using do...while(false) to create a scope you can easily break out of. e.g.

  bool success = false;

  do {
    if (!someCondition) break;
    if (!otherCondition) break; 
    ...
    success = true;
  } while(false);
  
  if (!success) {
   ...
  }
I personally disliked it, plus it can lead to funky behavior under optimization.


Wow! This guy must have been told he wasn't allowed to use goto anymore. The do-while block here is just a nameless replacement for the label.


The example shouldn’t be compared to esleif, that’s not how switches work.

Also, because it throws you can just use if, without a block. Or use if at the end of the line if your language supports that.

Way cleaner (less indentation), and less error prone. Switch statements only exist because of the underlying assembly/opcode.

It just as bad as goto, because it IS goto. The cases are goto-labels. It behaves like goto, and will simply generate the same JE/JNE jumps


It's interesting, but I can't decide if it's an anti-pattern or not. You're abusing a construct to achieve a slight improvement in brevity/readability, with the downside of JS's lack of block-scoping for case statements which means variables in one case can conflict with variables in other cases

All in all: I probably won't be using it


While readable and aesthetically pleasing, I find myself wondering about the performance implications of switch(true) versus a multi-branched if-else. Does V8 (and PHP) treat each construct differently when it comes to optimizations? We're not in C-land here, so jump tables are presumably not in play.


``` switch (true) { case 1 + 1 === 2: // This case evaluates to true so it will be executed default: // This will not be executed } ```

This is wrong, Since there is no return or break default will also be executed.


I prefer this pattern over long if else chains but I can never convince colleagues of this so I save this for code that I fully own


— What bad patterns of JavaScript programming do you know?

— Programming in JavaScript is a bad pattern itself.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: