How to make Rust as vertical as possible?

Hi. I am a beginner. Please don't harass me for the sake of my mental health. It's already going down very quickly since the day I learned about Rust.

I have two questions. I think that these two may be related.

I once heard that vertical code is easier to follow along. In other words, one should ignore nesting as much as possible. I believe everyone would agree on this.
Consider the following example from a C++ like language:

void do_something() {
    if (condition) {
        // do something
    } else {
        return;
    }
}

A cleaner version of the above can be:

void do_something() {
    if (!condition) return;
    // do something
}

I personally prefer this style and am used to it.
But it looks impossible to do in Rust.
I mean, every here and there we have let Some(x) = Option<_> and it just needs to have those curly braces.
Then we have the question mark (?) operator. As we all know, it is used to propagate errors.
Suppose we have 10 functions. Second function calling the first one, third one calling the second one, and so on...
Suppose only the first function gives a meaningful error and all other functions just propagate it to higher-number functions.
So the last function would just know that there is some error occurred, but it's just too deep. So, it will require keeping too much context in mind.
As an API user, I don't want errors corresponding to very deep functions.
In my opinion, it would be better if we have error message for each of those functions.
So, here, again comes issue, how to make Rust vertical?

It would help a lot if you gave concrete snippets of code you don't like. It's hard to know where to begin without knowing where you're at.

As for error handling, you can... handle errors? If function 2 has a better way to report the error from function 1, then it's on you to make function 2 do that (perhaps with map_err).

Rather than aiming to make your code vertical, you should aim to make it as much readable as possible.

Your code is meant to be read by at least one human: yourself. Never forget that.

The equivalent would simply be:

fn do_something() {
    if !condition {
        return;
    }
    // do something
}

Likewise, it's common to see:

let Some(whatever) = option else { return /*or `break` or `continue`*/ };

As already mentioned, for handling errors, when possible, I use result.map_err(|err| err.add_context())? instead of just result?. Sometimes, the added context would need ownership over some !Copy variable that I still need to use in the success case, in which case I do:

let success = match result {
    Ok(success) => success,
    // This avoids needing to call `Clone::clone` or similar on `owned_value`.
    Err(err) => return Err(ErrorWithContext(owned_value, err)),
};

? propagating errors without context can be a problem, but is fixable.

There's .ok_or(Error::Foo) that converts Option to a Result with an error type that can carry more context. For results there's .map_err(…) that gives you an opportunity to add more context to an existing error.

There's a bunch of similar helper methods and you can have a very tight control over this if you want, by using different error types.

If you don't want to write your own boilerplate for custom errors, there are libraries like anyhow that do it for you and let you write .context("whatever") on any Option or Result, so you can provide all the context to build breadcrumbs of what went wrong.

Generally, yes.

I think the "as much as possible" part is problematic. Deeper nesting is often harder to read than shallower nesting -- sometimes much harder. But you need some nesting to convey the meaning of the code, to make it understandable.

I used to write C code like that. As I got more experienced, I started writing:

if (!condition)
    return;

Yes, that's got a level of indentation. I think that's useful because the return is really important. If the code takes that path, the rest of the function won't execute. Having the return on a separate line makes it more visually obvious. It is indented because that helps convey that it is inside a conditional.

And eventually I switched to:

if (!condition)
{
    return;
}

The braces are not strictly required in C. But leaving them out can lead to subtle bugs. Consider:

if (!condition)
    *output = 0;
    return;

Someone quickly reading that code may expect both the assignment and the return to happen if condition is false, purely based on the indentation. If I later notice the lack of braces or inconsistent indentation, I have to stop and think carefully about what the intended behavior was. If you always use braces (like Rust requires), it is harder to make that kind of mistake. (Yes, I know that modern IDEs with automatic code formatting, and compiler warnings about unreachable statements, make this specific example less relevant.)

I'd say you're better off focusing on whether the code is readable and understandable. Several levels of indentation may be perfectly reasonable, as long as the reader can reasonably easily tell what code flow got them to that point. Comments like, "If we get here, it means..." (with a domain-specific meaning, like "the number is prime") can be very helpful.

You can reduce levels of indentation by extracting some of the indented code into a function. But be careful with that. If you're doing that purely to reduce the maximum indentation, and you're only calling it from one place, then seeing a function call and having to go figure out what it's actually doing, can be harder to understand than leaving the deeper nesting.