DEV Community

Cover image for Please don't write confusing conditionals
Basti Ortiz
Basti Ortiz

Posted on

Please don't write confusing conditionals

Have you ever spent more than a few seconds staring at the same line of code? How often did you find conditionals that were tricky to parse? In this article, we discuss how confusing conditionals manifest in our code. Along the way, we also explore different refactoring techniques for improving the readability of conditional control flows.

Double Negatives

A double negative arises when we negate a variable whose name is already in a non-affirmative style. Consider the following example.

// Non-affirmative Naming Convention
const isNotReady = doSomething();
const isForbidden = doSomething();
const cannotJoinRoom = doSomething();
const hasNoPermission = doSomething();

// Negating once results in an awkward double negative situation.
console.log(!isNotReady);
console.log(!isForbidden);
console.log(!cannotJoinRoom);
console.log(!hasNoPermission);
Enter fullscreen mode Exit fullscreen mode

This is opposed to the affirmative style, which requires less cognitive load to parse because one does not need to jump through the hoops of negating the variable name to extract the essence of its logic. In other words, the intent behind an affirmative variable may be read as isβ€”no hoops required! Consider the following modifications to the previous example.

// Affirmative Naming Convention
const isReady = doSomething();
const isAllowed = doSomething();
const canJoinRoom = doSomething();
const hasPermission = doSomething();

// Negation actually makes sense here!
console.log(!isReady);
console.log(!isAllowed);
console.log(!canJoinRoom);
console.log(!hasPermission);
Enter fullscreen mode Exit fullscreen mode

Unfortunately, non-affirmative naming conventions are sometimes inevitable due to standards and backwards-compatibility. Take the HTMLInputElement#disabled property of the HTML DOM APIs for example. The presence of the disabled attribute in an <input> tag tells the browser to (visually and literally) disable the element's form controls. Otherwise, its absence causes the <input> to exhibit its default behavior, which is to accept user input. This is an unfortunate side effect of the ergonomics of HTML.

<!-- Normal Checkbox (Default) -->
<input type="checkbox" />

<!-- Disabled Checkbox -->
<input type="checkbox" disabled />
Enter fullscreen mode Exit fullscreen mode

Nevertheless, we should still strive for affirmative naming conventions wherever possible. They are easier to read and parse simply because there is no need to mentally negate the variable name at all times. This rule applies to both variables names and function names alike.

Non-affirmative Control Flows

The next form of a double negative is a little bit more subtle.

// Suppose that we _do_ follow affirmative naming conventions.
const isAllowed = checkSomething();
if (!isAllowed) {
    // There is something funny about this...
    doError();
} else {
    // Notice how the `else` block is practically a double negation?
    doSuccess();
}
Enter fullscreen mode Exit fullscreen mode

As seen above, the non-affirmative style can also pervade conditional control flow. Recall that an else block is practically a negation of the corresponding if condition. We must therefore extend the affirmative style here. The fix is actually rather simple.

// Just invert the logic!
const isAllowed = checkSomething();
if (isAllowed) {
    doSuccess();
} else {
    doError();
}
Enter fullscreen mode Exit fullscreen mode

The same rule applies to equality and inequality checks.

// ❌ Don't do this!
if (value !== 0) {
    doError();
} else {
    doSuccess();
}

// βœ… Prefer this instead.
if (value === 0) {
    doSuccess();
} else {
    doError();
}
Enter fullscreen mode Exit fullscreen mode

Some may even go as far as to let a conditional block be blank just to negate a condition in affirmative style. Although I am not advocating for everyone to take it this far, I can see why this may be more readable for some people. Take the instanceof operator for example, which cannot be easily negated without parentheses.

if (obj instanceof Animal) {
    // Intentionally left blank.
} else {
    // Do actual work here (in the negation).
    doSomething();
}
Enter fullscreen mode Exit fullscreen mode

Exceptions for Early Returns

As a quick aside, there are special exceptions for conditional control flows that return early. In such cases, the negation may be necessary.

if (!isAllowed) {
    // Return early here.
    doError();
    return;
}

// Otherwise, proceed with the success branch.
doSuccess();
Enter fullscreen mode Exit fullscreen mode

Wherever possible, though, we should still attempt to invert the logic if it results in lesser nesting, fewer levels of indentation, and more readable affirmative styles.

// Prefer affirmative early returns.
if (isAllowed) {
    doSuccess();
    return;
}

// If we did not invert the logic, this would have been
// nested inside the `!isAllowed` conditional block.
if (!hasPermission) {
    doPermissionError();
    return;
}

// When all else fails, do something else.
doSomethingElse();
return;
Enter fullscreen mode Exit fullscreen mode

Another way to express the same control flow in an affirmative style (without early returns) is as follows.

// Hooray for the affirmative style!
if (isAllowed) {
    doSuccess();
} else if (hasPermission) {
    doSomethingElse();
} else {
    doPermissionError();
}
return;
Enter fullscreen mode Exit fullscreen mode

Of course, there are plenty of other ways to swap, invert, and refactor the codeβ€”the merits for each are totally subjective. Preserving the affirmative conventions thus becomes some kind of an art form. In any case, code readability will always improve as long as we uphold the general guidelines of the affirmative style.

Compound Conditions

The story gets a little bit more complicated with logical operators such as AND and OR. For instance, how do we refactor the code below in a more affirmative style?

// This is fine... but there has to be a better way,
// right? There are just too many negations here!
if (!isUser || !isGuest) {
    doSomething();
} else {
    doAnotherThing();
}
Enter fullscreen mode Exit fullscreen mode

For compound conditionals, we introduce the most underrated law of Boolean algebra: De Morgan's Laws!

// Suppose that these are **any** two Boolean variables.
let a: boolean;
let b: boolean;

// The following assertions will **always** hold for any
// possible pairing of values for `a` and `b`.
!(a && b) === !a || !b;
!(a || b) === !a && !b;
Enter fullscreen mode Exit fullscreen mode

Thanks to De Morgan's Laws, we now have a way to "distribute" the negation inside a condition and then "flip" its operator (from && to || and vice-versa).

Although the following examples only feature binary comparison (i.e., two elements), De Morgan's Laws are generalizable over any number of conditional variables as long as we respect operator precedence. Namely, the && operator is always evaluated first before the || operator.

// By De Morgan's Laws, we can "factor out" the negation as follows.
if (!(isUser && isGuest)) {
    doSomething();
} else {
    doAnotherThing();
}
Enter fullscreen mode Exit fullscreen mode
// Then we simply invert the logic as we did in the previous section.
if (isUser && isGuest) {
    doAnotherThing();
} else {
    doSomething();
}
Enter fullscreen mode Exit fullscreen mode

Now, isn't that much more readable? Using De Morgan's Laws, we can clean up conditionals that have "too many negations".

Conclusion

The overall theme should be apparent at this point. Wherever possible, we should avoid writing code that forces the reader to jump through hoops that (needlessly) necessitate extra cognitive overhead. In this article, we discussed the following techniques:

  1. Encourage affirmative naming conventions.
    • Avoid negative terms/prefixes like no, not, dis-, mal-, etc.
    • Prefer the positive equivalents.
  2. Invert conditional control flow (where possible) to accommodate for the affirmative style.
    • Feel free to play around when swapping, inverting, and refactoring branches.
    • Early returns may necessitate negations.
  3. Use some tricks from Boolean algebra to invert condtionals.
    • De Morgan's Laws are especially powerful tools for refactoring!

Now go forth and bless the world with cleaner conditionals!

Top comments (18)

Collapse
 
blueberry077 profile image
Marc-Daniel DALEBA

You are right. ヽ(γƒ»βˆ€γƒ»)οΎ‰
But in general it's better to write this instead:

if (!(!(!(!(!(!(!(!(isAllowed))))))))) {
    doSuccess();
}
Enter fullscreen mode Exit fullscreen mode

(οΏ£Ο‰οΏ£)

Collapse
 
somedood profile image
Basti Ortiz

Ah yes... a glorious piece of code right there. 🀣

Collapse
 
dipendracreator profile image
Dipendra Bhardwaj

Ye kya harkat hai

Collapse
 
lionelrowe profile image
lionel-rowe

Affirmative is a good rule of thumb, but I think in some cases, such as HTML's disabled, it makes more sense in the negative, as the usual assumption is that an element is enabled. Which is clearer in intent?

<!-- "negative" naming convention -->
<input name="username">
<input name="password">
<button disabled title="Enter the username and password first!">Submit</button>

<!-- "positive" naming convention -->
<input enabled name="username">
<input enabled name="password">
<button title="Enter the username and password first!">Submit</button>
Enter fullscreen mode Exit fullscreen mode

BTW here's a handy higher-order function when you want to convert a boolean-returning function into its opposite:

function not<T>(fn: (arg: T) => boolean) {
    return (arg: T) => !fn(arg)
}
Enter fullscreen mode Exit fullscreen mode

Usage:

function isOdd(n: bigint) {
    return Boolean(n % 2n)
}

const isEven = not(isOdd)
const nums = [1n, 2n, 3n, 4n]
const odds = nums.filter(isOdd) // [1n, 3n]
const evens = nums.filter(isEven) // [2n, 4n]
Enter fullscreen mode Exit fullscreen mode
Collapse
 
dkechag profile image
Dimitrios Kechagias

Maybe it's time for js to add the "unless" statement that Perl (and Ruby) has? We use a lot of Perl and usually our junior developers come from a javascript background, and the unless statement is one of the things they instantly like.

unless ($isUser && $isGuest) {
    doSomething();
}
Enter fullscreen mode Exit fullscreen mode

Or if you don't need to continue with else statements, you can do the postfix form which developers coming from other languages also seem to quickly find appealing:

doSomething() unless $isUser && $isGuest;
Enter fullscreen mode Exit fullscreen mode

Imagine the above without Perl's $sigils of course, but you can see how it allows an extra level of "positivity".

Collapse
 
somedood profile image
Basti Ortiz

I must admit that I was initially skeptical (to say the least) when I saw the syntax, but that neat one-liner piqued my interest! Personally, that's gonna take some getting-used-to on my end, but I can definitely see why one would "instantly like" such a feature.

Collapse
 
dkechag profile image
Dimitrios Kechagias

And I am with you about early returns. I like to avoid them, except if that will cause a lot of indentation and general ugliness, but I don't think twice about adding them when it comes to this pattern:

return unless $isUser;
Enter fullscreen mode Exit fullscreen mode

Feels natural. I don't use much javascript, but I see how it evolves nicely, so maybe something like that would not be a stretch to see added ;)

Collapse
 
efpage profile image
Eckehard

Some JS programmers make extensive use of the ternary operator istead of conditionals to make their code "more compact". Carefully used, this can lead to readable code:

   // conditionals
   if (fruit === "apple")
       color = "green"
   else
       color = "red"

   // ternary operator
   color = (fruit === "apple") ? green : red
Enter fullscreen mode Exit fullscreen mode

If you misuse the ternray operator as a short form of a conditional, this may make your code virtually unreadable:

something = condition ? nested_condition ? value_if_both_conditions_are_true
    : value_if_nested_condition_is_false : value_if_condition_is_false;
Enter fullscreen mode Exit fullscreen mode

Here is a real life example:

let getPropDescriptor = proto => proto ?
      Obj.getOwnPropertyDescriptor(proto, k) ?? getPropDescriptor(protoOf(proto)) :
      _undefined
Enter fullscreen mode Exit fullscreen mode
Collapse
 
pidgey0403 profile image
Gabrielle Niamat

Totally agree with using the affirmative style for writing conditionals - the logical flow makes much more sense and is very intuitive to follow. Curious to know your thoughts on using the ternary operator in Python or JavaScript?

Collapse
 
somedood profile image
Basti Ortiz

I don't mind them at all as long as they don't go two levels deep and beyond. πŸ˜…

Of course, the affirmative style must still follow from that.

name = 'admin' if is_admin else 'user'
Enter fullscreen mode Exit fullscreen mode
const name = isAdmin ? 'admin' : 'user';
Enter fullscreen mode Exit fullscreen mode
Collapse
 
ant_f_dev profile image
Anthony Fung

I think ternary statements are useful for assigning const values where there is some conditional logic to it, like in your example above. I personally find it even easier to read if the two values are put on different lines (but I realise this will be subjective to styling preferences).

const name = isAdmin
  ? 'admin'
  : 'user';
Enter fullscreen mode Exit fullscreen mode

There's only one reason I'll used nested ternaries: when writing variables in a dynamic HTML template and a return value must be computed in a single statement. Of course the workaround there is to compute it above the template where possible and then use it later.

Collapse
 
dyllanjrusher profile image
DyllanUsher

I feel like the example with

Not disallowed throw err
Else success path

Is a common way to structure your code to validate and clean up the success path conditions. Context is important though, I think overall it's more important to discuss the pattern with your team and try to come up with some pattern with justification. Having to negate things is not necessarily a good idea sometimes, but could make sense given the limitations of language, other context around the requirements, or existing patterns in the system.

Collapse
 
zirkelc profile image
Chris Cook

Interesting topic and well explained! πŸ‘πŸ»

Collapse
 
stalwartcoder profile image
Abhishek Mishra

I like how you making this series πŸ‘
You can also include don't about commit message as well in this series :)

Collapse
 
somedood profile image
Basti Ortiz

Thanks! I will certainly put this in my to-do list. πŸ™‡β€β™‚οΈ

Collapse
 
catsarebetter profile image
Hide Shidara

It's wild how many of these bad conditionals are in production code at top tier startups

Collapse
 
tracygjg profile image
Tracy Gilmore

Readers of this article might be interested in my post on De Morgan's Law.
dev.to/tracygjg/de-morgans-law-a-l...

Collapse
 
mylazuardy profile image
Ardy Lazuardy

agree! but it takes a lot of time and requires coding experience. happened to me :sad