loading...
Cover image for 5 ways to refactor if/else statements in JS functions

5 ways to refactor if/else statements in JS functions

sylwiavargas profile image Sylwia Vargas ・4 min read

In this blog post I will present 5 ways to declutter your code getting rid of unnecessary if-else statements. I will talk about:


1. Default parameters

You know that feeling when you're working with inconsistent API and your code breaks because some values are undefined?

 let sumFunctionThatMayBreak = (a, b, inconsistentParameter) => a+b+inconsistentParameter

sumFunctionThatMayBreak(1,39,2) // => 42
sumFunctionThatMayBreak(2,40, undefined) // => NaN
Enter fullscreen mode Exit fullscreen mode

I see that for many folks the instinctive solution to that problem would be adding an if/else statement:

 let sumFunctionWithIf = (a, b, inconsistentParameter) => {
    if (inconsistentParameter === undefined){
      return a+b
    } else {
     return a+b+inconsistentParameter
    }
}

sumFunctionWithIf(1,39,2) // => 42
sumFunctionWithIf(2,40, undefined) // => 42
Enter fullscreen mode Exit fullscreen mode

You could, however, simplify the above function and do away with the if/else logic by implementing default parameters:

 let simplifiedSumFunction = (a, b, inconsistentParameter = 0) => a+b+inconsistentParameter

simplifiedSumFunction(1, 39, 2) // => 42
simplifiedSumFunction(2, 40, undefined) // => 42
Enter fullscreen mode Exit fullscreen mode

2. OR operator

The above problem not always can be solved with default parameters. Sometimes, you may be in a situation when you need to use an if-else logic, especially when trying to build conditional rendering feature. In this case, the above problem would be typically solved in this way:

let sumFunctionWithIf = (a, b, inconsistentParameter) => {
    if (inconsistentParameter === undefined || inconsistentParameter === null || inconsistentParameter === false){
      return a+b
    } else {
     return a+b+inconsistentParameter
    }
}

sumFunctionWithIf(1, 39, 2) // => 42
sumFunctionWithIf(2, 40, undefined) // => 42
sumFunctionWithIf(2, 40, null) // => 42
sumFunctionWithIf(2, 40, false) // => 42
sumFunctionWithIf(2, 40, 0) // => 42
/// 🚨🚨🚨 but:
sumFunctionWithIf(1, 39, '') // => "40"
Enter fullscreen mode Exit fullscreen mode

or this way:

  let sumFunctionWithTernary = (a, b, inconsistentParameter) => {
    inconsistentParameter = !!inconsistentParameter ? inconsistentParameter : 0
    return a+b+inconsistentParameter
}

sumFunctionWithTernary(1,39,2) // => 42
sumFunctionWithTernary(2, 40, undefined) // => 42
sumFunctionWithTernary(2, 40, null) // => 42
sumFunctionWithTernary(2, 40, false) // => 42
sumFunctionWithTernary(1, 39, '') // => 42
sumFunctionWithTernary(2, 40, 0) // => 42
Enter fullscreen mode Exit fullscreen mode

However, you could simplify it even more so by using the OR (||) operator. The || operator works in the following way:

  • it returns the right-hand side when the left-side is a falsey value;
  • and returns the left-side if it's truthy.

The solution could then look as following:

  let sumFunctionWithOr = (a, b, inconsistentParameter) => {
    inconsistentParameter = inconsistentParameter || 0
    return a+b+inconsistentParameter
}

sumFunctionWithOr(1,39,2) // => 42
sumFunctionWithOr(2,40, undefined) // => 42
sumFunctionWithOr(2,40, null) // => 42
sumFunctionWithOr(2,40, false) // => 42
sumFunctionWithOr(2,40, '') // => 42
sumFunctionWithOr(2, 40, 0) // => 42
Enter fullscreen mode Exit fullscreen mode

3. Nullish coalescing

Sometimes, however, you do want to preserve 0 or '' as valid arguments and you cannot do that with the || operator, as visible in the above example. Fortunately, starting with this year, JavaScript gives us access to the ?? (nullish coalescing) operator, which returns the right side only when the left side is null or undefined. This means that if your argument is 0 or '', it will be treated as such. Let's see this in action:

  let sumFunctionWithNullish = (a, b, inconsistentParameter) => {
    inconsistentParameter = inconsistentParameter ?? 0.424242
    return a+b+inconsistentParameter
}

sumFunctionWithNullish(2, 40, undefined) // => 42.424242
sumFunctionWithNullish(2, 40, null) // => 42.424242
/// 🚨🚨🚨 but:
sumFunctionWithNullish(1, 39, 2) // => 42
sumFunctionWithNullish(2, 40, false) // => 42
sumFunctionWithNullish(2, 40, '') // => "42"
sumFunctionWithNullish(2, 40, 0) // => 42
Enter fullscreen mode Exit fullscreen mode

4. Optional chaining

Lastly, when dealing with inconsistent data structure, it is a pain to trust that each object will have the same keys. See here:

  let functionThatBreaks = (object) => {
    return object.name.firstName
  }

  functionThatBreaks({name: {firstName: "Sylwia", lasName: "Vargas"}, id:1}) // ✅ "Sylwia" 
  functionThatBreaks({id:2}) // 🚨 Uncaught TypeError: Cannot read property 'firstName' of undefined 🚨 
Enter fullscreen mode Exit fullscreen mode

This happens because object.name is undefined and so we cannot call firstName on it.

Many folks approach such a situation in the following way:

  let functionWithIf = (object) => {
    if (object && object.name && object.name.firstName) {
      return object.name.firstName
    }
  }

  functionWithIf({name: {firstName: "Sylwia", lasName: "Vargas"}, id:1) // "Sylwia"
  functionWithIf({name: {lasName: "Vargas"}, id:2}) // undefined
  functionWithIf({id:3}) // undefined
  functionWithIf() // undefined
Enter fullscreen mode Exit fullscreen mode

However, you can simplify the above with the new fresh-off ECMA2020 JS feature: optional chaining. Optional chaining checks at every step whether the return value is undefined and if so, it returns just that instead of throwing an error.

  let functionWithChaining = (object) => object?.name?.firstName 

  functionWithChaining({name: {firstName: "Sylwia", lasName: "Vargas"}, id:1}) // "Sylwia"
  functionWithChaining({name: {lasName: "Vargas"}, id:2}) // undefined
  functionWithChaining({id:3}) // undefined
  functionWithChaining() // undefined
Enter fullscreen mode Exit fullscreen mode

5. No-else-returns and guard clauses

Last solution to clunky if/else statements, especially those nested ones, are no-else-return statements and guard clauses. So, imagine we have this function:

  let nestedIfElseHell = (str) => {
    if (typeof str == "string"){
      if (str.length > 1) {
        return str.slice(0,-1)
      } else {
        return null
      }
    } else { 
      return null
    }
  }

nestedIfElseHell("") // => null 
nestedIfElseHell("h") // => null
nestedIfElseHell("hello!") // => "hello"
Enter fullscreen mode Exit fullscreen mode

✨ no-else-return

Now, we could simplify this function with the no-else-return statement since all we are returning is null anyway:

  let noElseReturns = (str) => {
    if (typeof str == "string"){
      if (str.length > 1) {
        return str.slice(0,-1)
      }
    }

    return null
  }

noElseReturns("") // => null 
noElseReturns("h") // => null
noElseReturns("hello!") // => "hello"
Enter fullscreen mode Exit fullscreen mode

The benefit of the no-else-return statement is that if the condition is not met, the function ends the execution of the if-else and jumps to the next line. You could even do without the last line (return null) and then the return would be undefined.

psst: I actually used a no-else-return function in the previous example 👀

✨ guard clauses

Now we could take it a step further and set up guards that would end the code execution even earlier:

  let guardClauseFun = (str) => {
    // ✅ first guard: check the type
    if (typeof str !== "string") return null
    // ✅ second guard: check for the length
    if (str.length <= 3) console.warn("your string should be at least 3 characters long and its length is", str.length) 
    // otherwise:
    return str.slice(0,-1)
  }

guardClauseFun(5) // => null 
guardClauseFun("h") // => undefined with a warning
guardClauseFun("hello!") // => "hello"

Enter fullscreen mode Exit fullscreen mode

What tricks do you use to avoid clunky if/else statements?

✨✨✨ If you are comfortable with OOP JS, definitely check this awesome blog post by Maxi Contieri!


Cover photo by James Wheeler from Pexels

Discussion

pic
Editor guide
Collapse
bugb profile image
bugb

Suppose you have a code like this:

function foo(arg) {
  if (arg === "a") return 1
  if (arg === "b") return 2
  return 3 //default value
}
Enter fullscreen mode Exit fullscreen mode

You can use something likes this instead of the example above or switch case:

const mapping = {
  a:  1.
  b:  2,
}
function foo(arg) {
  return mapping[arg] || 3;
}
Enter fullscreen mode Exit fullscreen mode
Collapse
sqlrob profile image
Robert Myers

Personally, I'm a little torn on this pattern. It's compact, and I've definitely used it.

But, IMHO, it increases the cognitive load of the code, you now have two places to look for what it's doing.

Collapse
eecolor profile image
EECOLOR

it increases the cognitive load of the code, you now have two places to look for what it's doing

For that reason in some instances I use this:

function foo(arg) {
  return (
    arg === 'a' ? 1 :
    arg === 'b' ? 2 :
    3
  )
}
Enter fullscreen mode Exit fullscreen mode

or this:

function foo(arg) {
  return {
    a:  1,
    b:  2,
  }[arg] || 3
}
Enter fullscreen mode Exit fullscreen mode
Collapse
greenroommate profile image
Haris Secic

And performance impact. Using a map (object whatever the name) should introduce indexing operation in the background. On the other hand what if arg = null or arg = 123 or so? You need to handle all the cases instead of having simple switch with the default. Or even just using if's until you actually need more than 3 cases.

Thread Thread
rasmusvhansen profile image
rasmusvhansen

I doubt there is any real world performance issue here. And as soon as you have more than 2 cases, this is much more readable than a bunch of if statements.

Regarding handling arg=null or arg=123, I don't think I see the issue.
That is handled by the

 return mapping[arg] || 3;
Enter fullscreen mode Exit fullscreen mode

part.

Thread Thread
greenroommate profile image
Haris Secic

I highly doubt that using objects instead of switches and ifs has same performance. I'm not limiting it to this scenario but talking about generic overview why should one avoid switches and ifs in favour of objects or hash maps. Reason I'm asking is because I know for a fact that compilers (including JIT ones) and maybe some interpreters have really good optimizations which can even remove branching in some cases but I have no idea would such thing kick in for objects in JS. I know that c++ compiler since 2017 or so has really good optimizations that can turn generics, branching, hahsmaps into few instructions all together. There was a nice conf were, I forgot his name, wrote code on left side and on the right there was assembly outout. I also know JIT in Java will sometimes kick in an replace some of your code with shorter instructions than on first complie. Question is will such things be done by WebKit or others for JS.
Reagarding the safety again it's about generic thing not thi particular piece. It's much easier to have switch with default case and even shorter than objects and relying that || will not be forgotten

Thread Thread
rasmusvhansen profile image
rasmusvhansen

I think we are both right here.
You are right that there probably is a difference in performance.
And I am right that in the real world it will make no difference*.
I learned to always prefer readability and then profile if something is slow. It is usually not what you think that is slowing you down.

*Except in that 0.001% weird edge case

Collapse
sylwiavargas profile image
Sylwia Vargas Author

Yeah, it's definitely less "user-friendly" or "beginner-friendly". Coming from Ruby, it gave me chills but also I was really excited for the shorthand properties so — I'm as torn as you are there!

Collapse
sylwiavargas profile image
Sylwia Vargas Author

Oh, yes! Totally — and thank you! I actually wanted to include this as well but had to run for a lecture. I'll include this wonderful example (I may just change the names so it's easier for tired folks to follow) in the blog tomorrow — look for the shoutout!

Collapse
yerlanyr profile image
yerlanyr

This thing I use a lot!

function foo(arg) {
  if (arg === "a") return 1
  if (arg === "b") return 2
  return 3 //default value
}
Enter fullscreen mode Exit fullscreen mode

At this point I always format if statements as if code block, because condition in if statement could be very long so that return would be lost dangling somewhere outside of field of view which could lead to bad mistakes.

function foo(arg) {
  if (arg % 3 === 0 && arg % 5 == 0 /* let's pretend there is very long condition*/) return 'foobar'
  if (arg % 3 === 0) return 'foo'
  if (arg % 5 === 0) return 'foo'
}
Enter fullscreen mode Exit fullscreen mode

Data structures are good for reducing code's complexity. Sometimes it is clearer to use switch case or if's.

Collapse
sylwiavargas profile image
Sylwia Vargas Author

I love these examples — thank you!

condition in if statement could be very long so that return would be lost dangling somewhere outside of field of view
I really couldn't agree more. This is definitely one of the main things that bother me about JS specifically — that there's no strict standard/expectation of writing really short code.

Collapse
aminnairi profile image
Amin

Hi there, great article, thank you!

I like to deal with all the error cases first, and then dealing with the heart of the function. Also I like to create small functions that deal with a single responsibility, this is easier to work with IMO.

I have sort of a kink with promises now and I like to put them everywhere. I like the simplicity of chaining functions.

"use strict";

const string = something => JSON.stringify(something);

const divide = (numerator, denominator) => new Promise((resolve, reject) => {
    if (typeof numerator !== "number") {
        return reject(new TypeError(`numerator should be a number in divide(${string(numerator)}, ${string(denominator)})`));
    }

    if (typeof denominator !== "number") {
        return reject(new TypeError(`denominator should be a number in divide(${string(numerator)}, ${string(denominator)})`));
    }

    if (0 === denominator) {
        return reject(new Error(`denominator should not be zero in divide(${string(numerator)}, ${string(denominator)})`));
    }

    return resolve(numerator / denominator);
});

divide(1, 2)
    .then(result => divide(result, 2))
    .then(result => divide(result, 2))
    .then(result => console.log(`After dividing by two three times: ${result}.`))
    // After dividing by two three times: 0.125.
    .catch(({message}) => console.error(`error while dividing: ${message}.`));

divide(1, 0)
    .then(result => divide(result, 2))
    .then(result => divide(result, 2))
    .then(result => console.log(`After dividing by two three times: ${result}.`))
    .catch(({message}) => console.error(`error while dividing: ${message}.`));
    // error while dividing: denominator should not be zero in divide(1, 0).
Enter fullscreen mode Exit fullscreen mode
Collapse
rasmusvhansen profile image
rasmusvhansen

I am not sure turning inherently synchronous code into async is a good idea. Promises are OK (not great - observables are better in many cases) for async operations but I would definitely avoid introducing the complexity for inherently sync operations.

By using this pattern, you are forcing all your code to be async.

Collapse
eecolor profile image
EECOLOR

If you use async your code becomes a bit more natural:

async function divide(numerator, denominator) {
    if (typeof numerator !== "number") {
        throw new TypeError(`numerator should be a number in divide(${numerator}, ${denominator})`);
    }

    if (typeof denominator !== "number") {
        throw new TypeError(`denominator should be a number in divide(${numerator}, ${denominator})`);
    }

    if (0 === denominator) {
        throw new Error(`denominator should not be zero in divide(${numerator}, ${denominator})`);
    }

    return numerator / denominator;
});
Enter fullscreen mode Exit fullscreen mode

This however indicates that it would be probably be smart to remove the async and move that to a separate function. That would allow you to use the divide function in more scenario's.

Collapse
sylwiavargas profile image
Sylwia Vargas Author

THANK YOU! YES, I was soooo tempted to touch on Promises but then majority of folks who read my blogs (maybe less so now?) are beginners so I'm planning to write a separate thing on promises in general. However, I'll link your comment in the article because I love your example! Thank you.

Collapse
dhintz89 profile image
Daniel Hintz

This is an awesome demonstration! I need to start learning and living this pattern for larger functionality.

Collapse
bjornet profile image
Björn Christensson

Tnx for this list.

Minor feedback in the ✨ guard clauses

guardClauseFun("h") // => undefined with a warning
Enter fullscreen mode Exit fullscreen mode

It will not return undefined since you do not break the function flow, it will continue to the end and return "" (empty string).

Collapse
vidhill profile image
David Hill

yup,
a solution would be to add a return before the console.warn then it would return undefined

if (str.length <= 3) return console.warn("your string should be at least 3 characters long and its length is", str.length) 
Enter fullscreen mode Exit fullscreen mode
Collapse
sylwiavargas profile image
Sylwia Vargas Author

Ah! True. I edited that part and didn't edit the examples. Argh I should write unit tests for my blogs 😩 Thank you!

Collapse
bjornet profile image
Björn Christensson

That is the way to go! Smart, if all code samples came from actual unit tests eg. Mocha you would also practice the mentality of "placing tests in the first room".

Thread Thread
sylwiavargas profile image
Sylwia Vargas Author

Yeah I might try it in some future blogs. I've always been a fan of TDD but never made space to properly go through all the phases and the process because I felt there was "not enough time". I'm slowly changing this mindset not only because I feel like TDD is really awesome but also because of how the "not enough time" mindset impacts me and my ability to code at times.

Collapse
merri profile image
Vesa Piittinen

Tip: instead of something === undefined && something === null you can write something == null and it matches both undefined and null, and only them.

I find it the only valid case to use == over ===.

Collapse
drarig29 profile image
Corentin Girard

That's true, but oftentimes you have a linter which forces to use "eqeqeq"

Collapse
merri profile image
Vesa Piittinen

Only if you have had someone strongly opinioned setting it up :) And even then you have the null: ignore option for it.

Collapse
sylwiavargas profile image
Sylwia Vargas Author

True! Thanks!

Collapse
cknott profile image
cknott

I think theres a typo in the last example of the 2nd block "OR operator"

this

  let sumFunctionWithOr = (a, b, inconsistentParameter) => {
    inconsistentParameter = inconsistentParameter ?? 0
    return a+b+inconsistentParameter
}
Enter fullscreen mode Exit fullscreen mode

should be

  let sumFunctionWithOr = (a, b, inconsistentParameter) => {
    inconsistentParameter = inconsistentParameter || 0
    return a+b+inconsistentParameter
}
Enter fullscreen mode Exit fullscreen mode
Collapse
sylwiavargas profile image
Sylwia Vargas Author

Ahhhh thank you! That's what happens when you copy examples mindlessly 😩

Collapse
vidhill profile image
David Hill

One thing I see on occasion,

is the resulting action being calling a function, with the only difference being some logic determining what a certain argument should be

const doSomething = function (arg1, arg2) {
  if (arg1 === "condition") {
    return callA(arg1, arg2, 10);
  }
  return callA(arg1, arg2, 0);
};
Enter fullscreen mode Exit fullscreen mode

versus

const doSomethingB = function (arg1, arg2) {
  const arg3 = arg1 === "condition" ? 10 : 0;
  return callA(arg1, arg2, arg3);
};
Enter fullscreen mode Exit fullscreen mode

Another one is calling functions with the same arguments, with the only difference being which function to call:

const doSomething = function (arg1, arg2, arg3) {
  if (arg1 === "condition") {
    return callA(arg1, arg2, arg3);
  }
  return callB(arg1, arg2, arg3);
};
Enter fullscreen mode Exit fullscreen mode

Versus

const doSomething = function (arg1, arg2, arg3) {
  const fnToCall = arg1 === "condition" ? callA : callB;
  return fnToCall(arg1, arg2, arg3);
};
Enter fullscreen mode Exit fullscreen mode
Collapse
sylwiavargas profile image
Sylwia Vargas Author

Thank you for these examples! Coming from Ruby, this is something I definitely instinctively lean towards and catch myself writing sometimes 😂 I'll include you're examples in the blog post!

Collapse
thexdev profile image
M. Akbar Nugroho

It's very awesome when you use guard clause to handle multiple condition instead of using if elseif else statement.

I'm a big fan of it and always use it to handle conditional statement in my code :).

Collapse
sylwiavargas profile image
Sylwia Vargas Author

Ahhhh I'm also such a fan of guard clauses! My first coding language is and was Ruby, which is obsessed with readable and pleasant code so of course, the looong code monsters that JS awakens from the deepest depth of programming hell gave me chills initially. I feel like my brain fries when I read nested if-elses... there should be an ESLint rule that obliges devs to draw a flowchart every time they write a nested if-else so it's quick and easy to follow :D

Collapse
mcsee profile image
Maxi Contieri

Nice!

This is applied to structured programming and these operators are great.

In OOP you can remove all accidental IFs and elses

Collapse
sylwiavargas profile image
Sylwia Vargas Author

Awesome! I've linked up your blog post in mine just now!

Collapse
kiddyxyz profile image
Muhamad Hudya Ramadhana

If you have more than one condition, why you just not using an array rather than or?

let arr = [null, undefined, '']
if (x in arr) {
   // code
}
Enter fullscreen mode Exit fullscreen mode
Collapse
sylwiavargas profile image
Sylwia Vargas Author

Yeah it's one way to do it but that still keeps the same if-else (or just no-return-statement) logic that we want to ideally get rid of. Here I'd also worry about the performance as if-else is costly and so is looping and here we'd have looping inside if-else. But I like this example — thank you!

Collapse
afpaiva profile image
Andre Paiva

Hi Sylwia. Great article, congrats!

Let me do some noob questions?
Using [switch] in a situation with many [if/else if], will offer a better performance?
And is there difference by using [switch] or [if] or vice versa on a compiled or non compiled programming language? Like, on C it would be better this, and on JS better that?

Thanks a lot!

Collapse
sylwiavargas profile image
Sylwia Vargas Author

Hi @afpaiva ! Thank you for your question!
In comparison with if/else statements, switch statements are a bit faster with small number of cases and get incrementally faster with new conditions. Readability, it seems that devs prefer to read if/else for two conditions and then switches for more.
There's this great table from O'reilly:
performance table comparing switches, if/else and table lookups

Talking about performance in C is both beyond my expertise and comfort level, sadly!

Collapse
abelardoit profile image
abelardoit

Hi there,

I would opt to reject any non-numeric value to compute any calculus.

If you provide a non-number to compute a sum, then the precondition is not valid; therefore, my function would throw an exception.

Nice article! Thanks for bring it us! Best regards.

Collapse
dankimhaejun profile image
Dankimhaejun

let nestedIfElseHell = (str) => {
if (typeof str == "string"){
if (str.length > 1) {
return str.slice(0,-1)
} else {
return null
}
} else {
return null
}
}


why don't you use like this?

let nestedIfElseHell = (str) => {
if (typeof str === "string && str.length >1) {
return str.slice(0,-1);
}

return null;
}

or
let nestedIfElseHell = (str) => {
if (isStringAndHasLength(str)) {
return str.slice(0,-1)
}

return null;
}

let isStringAndHasLength = (str) => typeof str === "string" && str.length > 1

Collapse
sylwiavargas profile image
Sylwia Vargas Author

Definitely! I like that you are using the typical for OOP callbacks to do the if/else job! Thanks!

Collapse
marln94 profile image
marln94

I've been using guard clauses for a while but I didn't know they were called like that!
Great post 🎉

Collapse
sylwiavargas profile image
Sylwia Vargas Author

Thank you! Yeah they definitely ✨ spark joy ✨

Collapse
gene profile image
Gene

"Geeezz, skip all these and just use typescript."

:/

Collapse
greenroommate profile image
Haris Secic

Since your shorting out the code I expected something like

let noElseReturns = (str) => {
  if (typeof str == "string" && str.length > 1) {
      return str.slice(0,-1)
  }
  return null
}
Enter fullscreen mode Exit fullscreen mode
Collapse
sylwiavargas profile image
Sylwia Vargas Author

yeah, that totally works too! Thanks for this comment!
I always try to balance how much I'm shortening the code vs how long the blog post is as my blogs posts are oftentimes read by folks with not much programming experience. Sometimes, I prefer to introduce changes incrementally or even not to introduce something because I also trust that the readers (just like yourself) would dig more or even find better solutions ✨

Collapse
morgvanny profile image
Morgan VanYperen

oh hi! I didn't realize I knew who wrote the article until I got to the comments lol. good post!

Collapse
sylwiavargas profile image
Sylwia Vargas Author

hello! ✨ good to see you here! it's funny how the tech world is both big and small 😂

Collapse
detunized profile image
Dmitry Yakimenko

Oh my! Poor Javascript developers.