Let-else syntax feels underwhelming

I tried to switch from match to let-else for my error handling, but not having access to the value in the else block severely limits what you can do: I can't log the error, convert it, or match on it.
Things are a bit better with Option, since with None you don't have a value. Still, I try to stick with match for consistency, as alongside you would often have matches on Results, and being consistent is more important than saving a few keystrokes.

While let-else is occasionally useful, the feature often feels underwhelming.
Not saying it isn't useful for others—just sharing my thoughts.

I'd be interested to hear what others think.

3 Likes

I assume you are actually talking about if let with optional else ? My understanding was always, that for the case that an else or even an else if is required, a match is the better alternative. See if let - Rust By Example

I presume they're talking about let-else.

3 Likes

Oh yes, I think I have already seen that syntax somewhere, but I think it is not covered in the official book or in the book by Jim Blandy.

No, I am talking specifically about let-else!

To be exact, quite often I would want to write code like this:

let Ok(value) = require() else {
    error!("Failed to require: {error}", error = ???);
    return Err(error);
};

let Some(result) = value.compute() else {
    error!("Failed to compute value: '{value}'");
    return Err(Error::CannotCompute);
};

...

which I can't, so I always go back to using match, even in the second case, because I want things to look the same.

1 Like

I wasn't sure what to make of it at first, too, but I've grown to like it and now I use it surprisingly often in cases where control flow diverges. For handling Result types I don't use it as frequently, mostly sticking to ? or actually handling the error.

9 Likes

It depends a lot on what I need to do with either part of the result, so

  • whether there's an actual value in Ok and in Err or not, and
  • whether the scope itself returns a Result / Option and supports ? or if it needs to be processed in place.

When I need to process both, I also usually prefer the match to let-else and methods that split the content. It leaves enough room to write the code for both alternatives and it's easier to read. But if there's a lot of code remaining for the Ok and the Err is anecdotical, I may prefer the if-else. Or even expect if the error is not supposed to happen or can't happen per design.

1 Like

Interesting, can you provide a quick example?

Even here let-else quite often is not enough, since else needs to diverge.

I like to use it in early-return scenarios to check some preconditions of the input. Something akin to assert!. Combining this with unwrapping a value at the same time is quite convenient I must admit. Example.

1 Like

I meant that, in case the error can be transferred with ? (or map_err(...)?), there's no need for an else or a match. For me, there isn't a generic pattern suitable for all situations.

3 Likes

It's a shortcut syntax for an extremely common thing in Rust: "give me a value when this pattern matches, and otherwise do this thing regardless of value." When you don't meet those criteria, then use a match. I don't really get the problem here.

Alternative syntaxes, including ones where else is not diverging or where you can match in the else block, were considered: 3137-let-else - The Rust RFC Book

14 Likes

If you want to log the error and still use let ... else, you can use Result::inspect():

As for your example code:

it might be a better idea to use the ? operator to convert the original error into the respective wrapping error type.

Alternatively, you can use a combinator style approach:

let result = require()
    .and_then(|value| value.compute().map_err(|_| Error::CannotCompute))?;
4 Likes

Yes, I know about those "trickery" functions, but I would always prefer language syntax to a soup of unwrap_or, and_then, map_err and now inspect_err!

But thanks! I didn't know about inspect_err before, might come in handy at some point.

3 Likes

I thought about this a bit more, and the main problem that I have is that let-else makes it very hard to pass around context on why errors are happening, and I am always trying to improve my errors to be as far away from file not found C-style error messages as possible, which, I feel like, let-else is in direct conflict with.

Don't use let-else when you need to handle errors. It's only for cases where you don't need the error (or in the case of Option<T> values, for example, when there is no error).

14 Likes

While l agree, the first line in RFC talks specifically about error handling.

let else simplifies some very common error-handling patterns.

1 Like

I'd focus on the some part of the quote. Because the next paragraph goes on to say (my emphasis):

if-let expressions offer a succinct syntax for pattern matching single patterns. This is particularly useful for unwrapping types like Option, particularly those with a clear “success” variant for the given context but no specific “failure” variant.

The "no specific failure variant" part seems to me to be the crux of this topic.

Also from the RFC:

let-else is particularly useful when dealing with enums which are not Option/Result, and as such do not have access to e.g. ok_or().

6 Likes

I use it as an early return counterpart to if let that would otherwise look like this:

let thing = if let SomeEnum::Thing(t) = thing {
    t
} else {
    return;
};

Sometimes with some return value or logging, but it's usually more a "nothing to do" case than an error case. I miss it when using C++ at work.

1 Like

As others said before, I don't use let-else to handle results.
For me, let-else is a way to reduce right drift due to nested blocks, since it makes the value accesible, opposed to if-let-else. So I use let-else only with Option.

For results, I often have
let x = result.inspect_err()?;
Note that there is no inspect_err for Option.

1 Like