Blog post

Stop nesting ternaries in JavaScript

Blog Author Phil Nash

Phil Nash

Developer Advocate JS/TS

7 min read

  • JavaScript
  • TypeScript
  • Clean Code
 An artistic impression of nested code with a big red cross over the top right corner.

Prettier, the most popular JavaScript code formatter, recently released a novel way to format nested ternaries under an experimental flag. This has come after years of disagreement over the best and most readable way to format a nested ternary.

I have a better idea of how to make nested ternaries clearer: stop nesting them.

What do you mean by nested ternary?

The ternary operator is an alternative to if/else for making decisions based on a condition. A regular ternary expression looks like:

const animalName = pet.canBark() ? "dog" : "cat";

A nested ternary is where you enter further ternary expressions in either the true or false branch.

const animalName =
  pet.canBark() ?
    pet.isScary() ?
      'wolf'
    : 'dog'
  : pet.canMeow() ? 'cat'
  : 'probably a bunny';

Let's take a look at why this isn't good practice.

Why nested ternaries are bad

I appreciate Prettier and all it has done to help us write consistently-formatted code. The project is opinionated, which prevents bike-shedding and lets us get on with the more important work of building projects. It promotes consistency, one of the four properties of Clean Code.  The least you can do if you are going to nest ternaries is to format them in a consistent manner.


I love that the Prettier project is working on making this better for all code, including nested ternaries. But looking over the hundreds of comments on how Prettier should format nested ternaries, I can't help but think that there will never be consensus on this topic.


Furthermore, another property of Clean Code is intentionality; code should be clear and straightforward. Nested ternaries are rarely clear or straightforward; I personally find them much harder to read and understand than other forms of conditionals.


We read code many more times than we write it, so it should be as easy to parse in one's head as possible. Picking your way through question marks and colons to determine what an expression means is much less clear than reading if/else statements that spell it out for you.


It's not just me who believes this; Sonar does not believe that nested ternaries are clear and straightforward, either. SonarQube, SonarCloud and SonarLint all enforce the rule that ternary operators should not be nested as part of the Sonar way profile.

Clearer ways

So, what do we replace a nested ternary with? Let's take the example nested ternary from Prettier's example and rewrite it a few different ways. Their example code, in their new format, is:

const animalName =
  pet.canBark() ?
    pet.isScary() ?
      'wolf'
    : 'dog'
  : pet.canMeow() ? 'cat'
  : 'probably a bunny';

Nested conditionals

The easiest way to replace a nested ternary is by turning them into a conditional

let animalName = 'probably a bunny';
if (pet.canBark()) {
  if (pet.isScary()) {
    animalName = 'wolf';
  } else {
    animalName = 'dog';
  }
} else if (pet.canMeow()) {
  animalName = 'cat';
}

In this example, we set up a variable that we will assign within the conditional statements. We can start the variable off with its default value, saving one else clause. Then, we apply the same logic as in the original example but with if/else blocks.


One of the issues that some developers have with this style is the use of the let variable. The ternary operator does have the benefit that it is an expression; that is, it returns a value. Meanwhile, if/else statements do not return anything, you need to either mutate a variable or use return.


If using a variable doesn't feel right, you can refactor the statement into its own function and use early returns.

function animalName(pet) {
  if (pet.canBark()) {
    if (pet.isScary()) {
      return "wolf";
    }
    return "dog";
  } else if (pet.canMeow()) {
    return "cat";
  }
  return "probably a bunny";
}

When you refactor the behaviour into its own function, you can independently test the functionality, ensuring that it is correct and cannot be broken unintentionally. And you can drop the extra variable assignment.

Reduce the nesting

The real issue in the code lies in the nesting itself. Nesting is one feature that makes code more complex.


In this example, we can refactor the nesting out to make this function easier to understand:

function animalName(pet) {
  if (pet.canBark() && pet.isScary()) { return "wolf"; }
  if (pet.canBark()) return "dog";
  if (pet.canMeow()) return "cat";
  return "probably a bunny";
}

Now, the function is clearer and uses fewer lines than the nested ternary we started with.


I like that this is a separate function that you can write tests for. But if it is important to you to include this behaviour in its original location, you can do so as an IIFE (instantly invoked function expression):

const animalName = (() => {
  if (pet.canBark() && pet.isScary()) { return "wolf"; }
  if (pet.canBark()) return "dog";
  if (pet.canMeow()) return "cat";
  return "probably a bunny";
})();

Personally, I like extracting the code to a separate function for the reasons I described above, plus I think the IIFE adds extra clutter. You may disagree, in which case the IIFE is an option.

In praise of nested ternaries?

Eric Elliott wrote in favour of nested ternaries, calling them "chained ternaries" once you perform operations on the conditions to ensure you only ever chain in the else clause of the ternary. His points about the difference between statements and expressions are good, and we should avoid side effects and mutation where we can, and expressions allow for that.


His first argument is that if/else statements permit you to write code that causes side effects. However, once you have reduced a conditional to code that you can write as a nested ternary, you can also avoid side effects and mutation by rewriting it as a function with returns, as we did above. On the other hand, you can absolutely write mutations and side effects within a ternary, so it seems to be a moot point to me.


His second argument is that the syntax of an if/else statement is clutter that takes up working memory, causes interference and leaves a greater surface area for bugs. For me, parsing a ternary requires me to come up with the syntax in my head, so even if the file doesn't say "if" and "else", my understanding of a ternary requires that. The idea that changing ifs and elses to question marks and colons gives you less surface area for bugs is a bit far-fetched for me. I'm not going to argue that using if/else statements will help you write more correct code and I discount that using fewer characters will achieve that either. I contend that if/else statements make the code clearer and easier to understand. And, for me personally, the effort of understanding and translating the ternary syntax actually increases my likelihood of introducing bugs during maintenance.

Is there any place for nested ternaries?

Those of you using JSX might well be fuming by this point.


There are no if/else statements in JSX, so you need to use the ternary operator to make decisions. It is then common to nest those operations when rendering components conditionally and including conditional attributes in code like this:

return (
  <>
    {isLoading ? (
      <Loader active />
    ) : (
      <Panel label={isEditing ? 'Open' : 'Not open'}>
        <a>{isEditing ? 'Close now' : 'Start now'}</a>
        <Checkbox onClick={!saving ? setSaving(saving => !saving) : null} />
      </Panel>
    )}
  </>
);

Of course, nested ternaries are necessary in JSX, so the above code is perfectly reasonable. My recommendation is still to minimise the nesting as best as you can. It's also worth noting that the Sonar rule for nested ternaries does not apply to JSX.

Minimise nesting and prioritise clarity over brevity

Nesting in code introduces complexity, so the first thing you should do when you find yourself with a nested ternary is to try to refactor to reduce the nesting as much as possible. Then, you can use any of the patterns above to remove the nested ternaries.


The one thing you can say about any of the alternatives above is that they all use more characters and involve more typing than the ternary-based solution. However, as I asserted earlier, code is read much more often than written.


The Prettier blog post describes if/else statements as "ugly", but I will always prefer ugly, understandable code over picking apart the question marks and colons in a nested ternary.


When you are intentional with your code and choose to minimise nesting and use fewer ternary operators, you will find your code is clearer and easier to understand and change over time. 


If you'd like a tool in your IDE to remind you of this as you type, install SonarLint for free. And if you're not already formatting your code, add Prettier to your project, too. Because if you do have nested ternaries, formatting them might improve the clarity of your code until you get around to refactoring them.

Get new blogs delivered directly to your inbox!

Stay up-to-date with the latest Sonar content. Subscribe now to receive the latest blog articles. 

By submitting this form, you agree to the storing and processing of your personal data as described in the Privacy Policy and Cookie Policy. You can withdraw your consent by unsubscribing at any time.

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.

SonarLint

Clean Code from the start in your IDE

Up your coding game and discover issues early. SonarLint takes linting to another level empowering you to find & fix issues in real time.

Install SonarLint
SonarQube

Clean Code for teams and enterprises

Empower development teams with a self-hosted code quality and security solution that deeply integrates into your enterprise environment; enabling you to deploy clean code consistently and reliably.

SonarCloud

Clean Code in your cloud workflow

Enable your team to deliver clean code consistently and efficiently with a code review tool that easily integrates into the cloud DevOps platforms and extend your CI/CD workflow.