Pre RFC ish: Bring ensure! macro from error-chain into std?


#1

I use error-chain a lot and I find the newly introduced ensure! macro very convenient. But I also maintain a couple of crates that does not use error-chain and I really miss the macro there. I could of course just add error-chain to those crates and use the macro, but I feel that this would fit very well in the standard library as well.

To me it really makes sense to have something like assert! but for returning errors instead of panicking.

Proposal: Introduce a macro equivalent to ensure! from error-chain 0.10.0 into libcore and libstd.

https://docs.rs/error-chain/0.10.0/error_chain/macro.ensure.html

I’m creating this thread to see if others would also find this usable. Getting some opinions from the error-chain maintainers/developers would be nice. Ping @brson @Yamakaky, and thanks for a great error handling crate :slight_smile:

Example

This code is a part of a function parsing an IPv4 packet to make sure the length and checksum headers are correct. Here presented simplified to just showcase what ensure! could bring to the table.

Without ensure!:

/// ...
if total_length > payload.len() || total_length < MINIMUM_SIZE {
    Err(RxError::InvalidLength)
} else {
    let ip_pkg = Ipv4Packet::new(&payload[..total_length]);
    if ip_pkg.get_checksum() != checksum(&ip_pkg) {
        Err(RxError::InvalidChecksum)
    } else {
        Ok(ip_pkg)
    }
}

With ensure!:

/// ...
ensure!(total_length <= payload.len() && total_length >= MINIMUM_SIZE,
        RxError::InvalidLength);
let ip_pkg = Ipv4Packet::new(&payload[..total_length]);
ensure!(ip_pkg.get_checksum() == checksum(&ip_pkg),
        RxError::InvalidChecksum);
Ok(ip_pkg)

#2

I wish it didn’t need a macro. Something like this:

fn foo(...) -> Result<Ipv4Packet,RxError> ? {
    if total_length > payload.len() || total_length < MINIMUM_SIZE {
        throw RxError::InvalidLength
    }
    let ip_pkg = Ipv4Packet::new(&payload[..total_length]);
    if ip_pkg.get_checksum() != checksum(&ip_pkg) {
        throw RxError::InvalidChecksum
    }
    ip_pkg
}

(modulo bike shedding for the syntax to turn on the throw keyword and catch-like auto-success wrapping)


#3

What’s wrong with early returns?

fn foo(...) -> Result<Ipv4Packet,RxError> ? {
    if total_length > payload.len() || total_length < MINIMUM_SIZE {
        return Err(RxError::InvalidLength)
    }
    let ip_pkg = Ipv4Packet::new(&payload[..total_length]);
    if ip_pkg.get_checksum() != checksum(&ip_pkg) {
        return Err(RxError::InvalidChecksum);
    }
    Ok(ip_pkg)
}

#4

I’m not sure I like how throw works here. It just desugars to return Err(...), whereas the keyword throw is pretty likely to make people think you’re doing something similar to what an exception would in languages like Java or C++ (maybe a panic with catch_unwind?).

That said, using conditional guards like that to do an early return is still quite easy to understand and read…

fn foo(...) -> Result<Ipv4Packet,RxError> {
    if total_length > payload.len() || total_length < MINIMUM_SIZE {
        return Err(RxError::InvalidLength);
    }
    let ip_pkg = Ipv4Packet::new(&payload[..total_length]);
    if ip_pkg.get_checksum() != checksum(&ip_pkg) {
        return Err(RxError::InvalidChecksum);
    }
    Ok(ip_pkg)
}

Having the ensure macro would be super convenient, but it’s easy enough to write your own if you need one.

macro_rules! ensure {
  ($cond:expr, $ret:expr) => {
    if !$cond {
      return Err($ret.into());
    }
  }
}

#5

Yes, it is very easy to write your own version. My point being if it’s something a lot of people find themselves doing quite often it would make sense to add to std. But maybe the early return versions without macros are nice enough. It’s probably just a hang-up I have, that I try to avoid early returns (except when they are hidden in macros and ?) :stuck_out_tongue:


#6

Good point. I imagine that’s how assert became part of the std library even though it’s just as simple as ensure and you can implement it yourself.

I just had a look through some of my existing projects and noticed that I often follow the pattern of using an early return to immediately deal with an edge case (e.g. if some_vec.len() == 0 { return Err(...); }), so I’d probably benefit from something like ensure.


#7

May be there is more complex example of usage of ensure!?

Becuase of as already pointed you can just use return keyword,
and remove all else


#8

Given how simple the macro is you can always do what it does manually in not too much extra code. So I’m not sure there examples that are much more complex. It simply gives a handy name to something common and shortens the code a bit.


#9

Or you could you the boolinator (by @DanielKeep) :

extern crate boolinator;
use boolinator::Boolinator;

fn foo(...) -> Result<Ipv4Packet,RxError> {
    (total_length > payload.len() || total_length < MINIMUM_SIZE).ok_or(RxError::InvalidLength)?;
    let ip_pkg = Ipv4Packet::new(&payload[..total_length]);
    (ip_pkg.get_checksum() != checksum(&ip_pkg)).ok_or(RxError::InvalidChecksum)?;
    Ok(ip_pkg)
}

#10

That is indeed short. But IMO less readable. A word like “assert” or “ensure” in the beginning gives the entire operation much more meaning and explanation.


#11

catch blocks landed recently, so the analogy is intentional, as catch { throw x; } would result in Err(x). (And, internally, ? is already doing exactly what throw would need to do–throw x is roughly Err::<!,_>(x)?, with perhaps different conversion semantics.)


#12

While it is simple to write ensure! by your self there are some reasons for placing it in std:

  • the import system is currently a bit meh, which discourages me every time to use custom macros of this type
  • ensure is the “err” counter part of assert having a “standard” way to do this can improve readability, etc.
    • but this also means it only makes sense if using ensure! also become the recommended style for this kind of situations

or maybe we just want to put up some crate which does not has a single line of code but collects all kind of <10 lines macros (or well just one per macro type/area, but then sometimes pooling makes thinks easier, like finding such macros, etc.)


#13

Should ensure! do the same conversion that try! does to coerce different error types to the expected type?