Let-else syntax feels underwhelming

or_else should work for Option!

Thanks! I might start using inspect_err more often, I didn't know about it at least!

2 Likes

To me, I think of let-else not as an error handling pattern, but more like where I'd write if (x != 0) continue; kinds of guards in C.

It's really nice in a loop for, say,

let Some(x) = ... else { continue };

As :sushi: said, if it doesn't fit for what you need, that's fine. It's just a "handy sometimes" syntactic sugar, not a "this should be the normal way for everything".

19 Likes

You might want to consider something like this:

let x = match result {
    Ok(x) => x,
    Err(e) => {
        log!(e);
        return Err(e);
    }
}

Maybe something like this could be possible with exhaustive matches.

let Ok(x) = result else {
    let Err(e) = result;
    log!(e);
    continue;
};
1 Like

Consider the code,

let items = [1,3,18];
    let Some(item) = items.iter().next()    else {
        println!{"there is no next"}; return
    };
    println!{"item: {item}"}

To which item do you consider to get access in the else? You get None for an Option so far. But if you are interested in different parts of Result, then use match. let else is very powerful operator allowing to avoid tons of nested ifs. I use it very heavy in my codebase. Think of it.

2 Likes

I agree with you, when using match in this case make Ok(o)=>o, seems a little bit too verbose.
So I write a this macro :

#[macro_export]
macro_rules! ok_else {
    ($a_result:expr,($err:ident)$manage_error:expr) => {
        match $a_result {
            Ok(o) => o,
            Err($err) => $manage_error,
        }
    };
}

used like this :

let sz = ok_else!(multipart.read(&mut buf) ,(err){
    error!("Error in read : {err}");
    return Some(p500());
});

but something like this could be great (but is not possible at this time) :

let Ok(sz) = multipart.read(&mut buf)  else (err) {
    error!("Error in read : {err}");
    return Some(p500());
});
2 Likes

Again, this is a use case for Result::inspect_err().

let Ok(sz) = multipart.read(&mut buf).inspect_err(|err| error!("Error in read : {err}")) else {
    return Some(p500());
};
4 Likes

In general i feel the same about let-else usefulness.

Often times i would like to use something like:

let Ok(result) = some_fn()  else unmatched_value {
    error!("Unexpected value: {:?}", unmatched_value);
    return
});

The thing is that at first, i don't need unmatched result, so i use let-else and it is a very useful syntax, it does not require to nest code so deep as match statement, code looks clean and is easier to read, but then at some point code evolves, maybe returned error value/enum gets more variants and i want to log them in error log, while still keeping the same code behavior, so, yeah, it's a dead end, have to rewrite and use match statement.

3 Likes

The main problem with inspect_err is that function callbacks in general do not work very well with ?, async, properly getting everything from the environment.
Also the way rustfmt formats callbacks for me doesn't feel very readable to the point where I would always prefer to use match instead.

Recently I saw that video, which, among other things, discussed how let-else is handled in Zig.
And I think the way it solves the problem is exactly what I was missing in Rust to start this discussion.

Yes but to be more generic about the subject, the problem with Result::inspect_err() is that it's not possible to use the Error value in the return.
More clearly this code does not compile :

let Ok(sz) = multipart.read(&mut buf).inspect_err(
   |err| return Some(p500(err.into()))
) {};

This code could be more shorter :

let sz = match multipart.read(&mut buf) {
   Ok(o) => o, // semantically useless line
   Err(err) => return Some(p500(err.into())),
};

...replacing by this code (from my phantasm) is really clear (IMHO), but just a dream :

let Ok(sz) = multipart.read(&mut buf) {
   Err(err) => return Some(p500(err.into())),
};

I will continue to use my old macro, praying for Rust gods to implement this latest function one day :slight_smile:

I think that is because when new concepts and syntax is added to rust, they try to keep the changes to syntax simple so it is not overwhelming.

Indeed, but this was not necessary in the case quoted by me.
If you need the error type as well as the success type, you want to use match.
let else is specifically meant as syntactic sugar for cases, where you only need the value of either.

1 Like

Recently I met the problem you described, but since I learned Rust a bit more than at time you created the thread, I solved it in the next way:

let Ok((mut data,bl_kind,last,mut extra,mask,mut mask_pos,remain)) = decode_block(&mut buffer[0..len + reminder])
                                            .or_else(|e| {LOGGER.lock().unwrap().error(&format!("decode bl err:{e}")); Err(e)}) else {
                                            debug!("invalid block of {}, WS's closing", len + reminder);
                                            break 'serv_ep
                                        };

The code is taken from the production branch, so it needs some explanation. Indeed, you have no access to Err part, but can easily to add it using: or_else.

What I mainly use let-else for is to reduce the amount of nesting, which in turn allows me to keep a smaller amount of context in my head while still comprehending the code.

In particular, if something needs to be destructured into a specific variant and no other variant will do, I use let else to both assert that the value is indeed that variant and gain access to the variant's data. If the value is not that variant, the else-clause returns from the fn.
Underneath that let-else I can just assume the happy path, thereby gaining the ability to forget some of the context that I would need to keep in my head if it had been a match rather than a let-else.

1 Like

let-else is just a syntactic sugar over match. Nothing stops you from forgetting the context in match either, as in:

let req = match data {
    Data::Req(req) => req,
    _ => return,
};

The idea of or_else is to try to recover from an error, what you are doing is just inspecting it, so
inspect_err would be better there.

I also don't understand why would you split logging into two parts, this is somewhat confusing to read and in the end the simple match is way cleaner, in my opinion.

let block = decode_block(&mut buffer[0..len + reminder]);
let (mut data, bl_kind, last, mut extra, mask, mut mask_pos, remain) = match block {
    Ok(block) => block,
    Err(error) => {
        LOGGER.lock().unwrap().error(&format!("decode bl err:{e}"));
        debug!("invalid block of {}, WS's closing", len + reminder);
        break 'serv_ep;
    }
};

It's perfectly possible to do so, yes. And before let-else that's often what I did.

But let-else has turned out to be more ergonomic for the use case I stated.

1 Like

For the code that I write I often regret using let-else, because it just doesn't scale.
After some time I would need to inspect the error message, log it somewhere, etc, which will force me to rewrite the statement with match.

In the end I found it quite useful to work with parsed data, where it is expected that some values can be ignored, but in the end I wish let-else was (somehow) more general. Similar to how orelse is in Zig.

For example, given the example from @MOCKBA above, I would really want to be able to write something like this to "take" a single case out of match:

let Ok(mut data) = block else {
    Err(error) => {
        log::error!("decode blk error: {error}");
        log::debug!("invalid block of {}, WS's closing", len + reminder);
        break 'serv_ep;
    }
};

With the ability to ignore the result as before:

let Ok(mut data) = block else {
    debug!("invalid block of {}, WS's closing", len + reminder);
    break 'serv_ep;
};

And maybe even recover in certain cases (though this is questionable).

let Ok(mut data) = block else {
    Err(error) => {
        let Ok(new_data) = try_to_recover(error) else {
            log::debug!("invalid block of {}, WS's closing", len + reminder);
            break 'serv_ep;
        };
        new_data
    }
};

But at that point, match just works.

let data = match block {
    Ok(data) => data,
    Err(error) => match try_to_recover(error) {
        Ok(new_data) => new_data,
        Err(_) => {
            log::debug!("invalid block of {}, WS's closing", len + reminder);
            break 'serv_ep;
        }
    }
};

It is exact the reason why I try to avoid match, because it easily makes a 12 levels of nesting in my code base hard readable.

Absolutely, it's a very good catch. We tried to convert certain errors in a specifically generated good result which pushed our parser to read more data and then to try to decode it again. Later, we decided to generate such result in the decoder itself, but forgot to change the method name. So, you're a member of our team now.

This is an interesting observation, so I conclude you didn't pass 1 million lines code in you profile yet. Later you will learn one fact easily picked up by AI:

Debug: This trait is for developers. It provides a detailed, unambiguous, and often technical representation of a type, primarily for debugging and internal logging.
Display: This trait is for end-users. It offers a clean, human-readable, and user-friendly representation of a type, suitable for UI, user-facing logs, or general output.

So we cover two cases here.