Skip to content

Instantly share code, notes, and snippets.

@ky28059
Last active March 10, 2023 22:29
Show Gist options
  • Save ky28059/84f33cee7cec0066390f79ee3629021e to your computer and use it in GitHub Desktop.
Save ky28059/84f33cee7cec0066390f79ee3629021e to your computer and use it in GitHub Desktop.

Short circuit evaluation is a type of lazy evaluation where a boolean expression with || or && can return early if the left-hand value suffices to determine the expression's value.

Conceptually, if the left-hand side of an || expression is true, we don't care about the right; no matter what the right-hand side evaluates to, we know the expression will always evaluate to true. Similarly, if the left-hand side of an && expression is false, we don't need to know what the right-hand side evaluates to as we know that the expression will always evaluate to false.

If the left-hand side of an || expression is false (or the right-hand side of an && expression true), the value of the expression depends only on what the right-hand side evaluates to. If you're having trouble visualizing this, use the following table:

false || true -> true
false || false -> false

true && true -> true
true && false -> false

Therefore, a shortcut exists for evaluating these expressions. For a || b, if a is true then return a. Otherwise, simply return b. Similarly, for a && b, if a is false then return a; otherwise, return b.

Why does this matter?

Because short-circuited checks return early if the left-hand information is sufficient to determine the result, when conditions are chained, short circuiting can be used to conditionally avoid harmful side effects in later conditions. The following examples are in Java:

// Return early if `user.name` is null, avoiding a null pointer exception.
if (user.name != null && user.name.length() > 5) {
    // ...
}
// Return early if the denominator is 0, avoiding a divide by zero exception.
if (denom != 0 && num / denom > 1) {
    // ...
}

Conceptually, an early return means that each check can establish certain "guarantees" for later conditions.

public boolean isFourthElementInvalid(String[] elements) {
    // `elements[3]` is guaranteed to exist for the second condition because we return early if 
    // elements.length < 4. Similarly, `elements[3].charAt(1)` is guaranteed to be safe because we 
    // return early if elements[3].length() < 2.
    return elements.length >= 4 
        && elements[3].length() >= 2 
        && elements[3].charAt(1) != 'V';
}

In TypeScript, the above can be used with type guards. Here, we guarantee that if the second condition is evaluated, obj is a number and is therefore safe to be compared with <.

// In the right-hand side of the condition, obj: number
if (typeof obj === 'number' && obj < 5) {
    console.log(`${obj} is less than 5`);
}

Truthiness and casting

In dynamically typed languages like JavaScript and Scheme, short circuit evaluation can be paired with the concept of "truthiness".

In such languages, in boolean conditions like || or &&, "falsy" values like '' (the empty string), 0, null, or undefined are automatically cast to false while all other values are considered "truthy" and cast to true. Because short circuited conditions return a and b instead of necessarily true or false, they can be used outside of boolean contexts to simplify common programming patterns.

For example, a common pattern is to conditionally assign default values for variables if the user doesn't supply a value themselves. One way to implement this is using a mutable variable whose value is changed inside an if statement:

async function ban(user: User, reason?: string) {
    let parsedReason = reason;
    // If reason is null or empty, use the string 'No reason provided' instead
    if (!parsedReason) parsedReason = 'No reason provided';
    await user.ban(parsedReason);
}

But this method may be unideal if it is preferable to keep the variable immutable. Conceptually, too, it shouldn't take two assignments to parsedReason to give it the default value when we know from the beginning that reason is undefined. One way to simplify this assignment is using a ternary expression:

async function ban(user: User, reason?: string) {
    const parsedReason = reason ? reason : 'No reason provided';
    await user.ban(parsedReason);
}

But reason ? reason seems repetitive and redundant. Indeed, the assignment can be further simplified using a short circuited ||:

async function ban(user: User, reason?: string) {
    // If reason is non-null and non-empty, use it; otherwise, use the string 'No reason provided'
    const parsedReason = reason || 'No reason provided';
    await user.ban(parsedReason);
}

In fact, this pattern is so common that most languages have a null coalescing operator ?? which performs this short circuit assignment specifically for when the left-hand value is nullish.

function printLocalStorageKey(key: string) {
    const data = localStorage.get(key);
    console.log(data ?? `LocalStorage key ${key} not found.`);
}

In React, short circuit evaluation powers conditional rendering:

export default function UserCard(props: User) {
    return (
        <div className="card">
            {/* If any of these properties are null, return and discard it. */}
            {/* Otherwise, render the corresponding tag. */}
            {props.avatar && <img src={props.avatar} />}
            {props.name && <h1>{props.name}</h1>}
            {props.email && <h2>{props.email}</h2>}
            {props.desc && <p>{props.desc}</p>}
        </div>
    )
}

Because the right-hand-side is never evaluated when an expression "short circuits", you can use it to simplify conditional code execution if (x) y() inside arrow functions or other expression contexts:

// Call `removeTodo(i)` if the input is checked, otherwise return early.
return <input type="checkbox" onChange={(e) => e.target.checked && removeTodo(i)} />
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment