Hi, I'm Eric.

I’m an avid world traveler, photographer, software developer, and digital storyteller.

I help implement the Content Authenticity Initiative at Adobe.

Fail fast

2 May 2026

When I’m writing a function, I think of the code as having two parts: the main logic (the “happy path”) and the error handling. I want the main logic to be the visual focus of the function. Error cases need to be acknowledged and dealt with, but they shouldn’t dominate the structure.

One reason this matters: I’ve come to think of indentation as a code smell. Each level of indentation is another piece of context I have to keep in my head while reading: another condition that’s true, another loop I’m inside, another branch I’m partway through. By the time I’m three or four levels deep, I’ve usually lost the thread. It’s not always wrong — some logic genuinely needs to be nested — but when I see a function drifting toward the right side of my screen, I take it as a signal to stop and ask: is there a flatter way to write this?

The simplest way to flatten error handling is to take care of it first and early. If something can go wrong, I check for it at the top of the function and exit immediately when it does. By the time the reader reaches the meat of the function, they know all the preconditions are satisfied — and the code that depends on them sits at the outermost indentation level, where the eye naturally lands.

An example

Suppose we’re parsing some input and then performing several steps with the result. Here’s how I don’t like to write it:

Example 1. Happy path is buried inside an if let
fn process(input: &str) -> Result<Output, Error> {
    if let Ok(parsed) = parse(input) {
        let validated = validate(&parsed)?;
        let normalized = normalize(&validated)?;
        let result = transform(&normalized)?;
        Ok(result)
    } else {
        Err(Error::ParseFailed)
    }
}

By the time I reach the else branch, I’ve forgotten what the original error was. I have to scroll back, find the if let, and reconstruct what failed. Worse, the meaningful part of the function — the four lines that actually do the work — is wrapped in an extra layer of indentation for no good reason.

Compare with this version:

Example 2. Error handled first; happy path is unindented
fn process(input: &str) -> Result<Output, Error> {
    let Ok(parsed) = parse(input) else {
        return Err(Error::ParseFailed);
    };

    let validated = validate(&parsed)?;
    let normalized = normalize(&validated)?;
    let result = transform(&normalized)?;
    Ok(result)
}

The error case is acknowledged up front and dispatched immediately. The rest of the function reads as a straight sequence of steps — no nesting, no else, no surprises.

Why this works

A couple of other things I like about this pattern:

  • Each error sits right next to the thing that could fail. I don’t have to mentally connect a return Err(…​) at the bottom of the function with the condition that triggered it.

  • Adding more steps is easy. When the happy path is a flat sequence of statements, I can append another step without rethinking the function’s structure.

In Rust, the let …​ else syntax makes this especially clean for cases that don’t fit the ? operator. For cases that do fit ?, the same idea applies: convert the error to the right type, propagate it, and keep moving.

Further reading

Keith J. Grant makes a similar argument in Simplify Nested Code, with examples in JavaScript. He frames it as “return early, return often,” and walks through how stacked conditionals can be flattened into a sequence of guard clauses. Different language, same instinct.

If you’ve enjoyed this …

Subscribe to my free and occasional (never more than weekly) e-mail newsletter with my latest travel and other stories:

Or follow me on one or more of the socials: