`?` operator for `()`

I couldn't find another post or a relevant RFC for this, but maybe I overlooked it because I'm not sure what the correct terms for looking are.

I notice that when I write code that handles errors of different code paths without returning an error itself, the code tends to be filled with match statements which only handle the error case, for example:

fn some_func() {
  let result = match something_fallible() {
    Ok(result) => result,
    Err(err) => {
      // .. do something with 'err'
      return;
    }
  };

  // .. do something with result
}

It would be nice if I can use ? in this case, reducing a lot of boilerplate:

fn some_func() {
  let result = something_fallible()
    .map_err(|err| {
      // .. do something with 'err'
    })?;

  // .. do something with result
}

This would work if ? (ErrorPropagationExpression) is implemented for (), since both .map_err(..) and some_func() in my example return ().

Advantages

  • Less boilerplate code
  • No need to assign arbitrary variable name for match branch in match val { Ok(val) => val; .. }

Possible Downsides

  • The current description of the ? operator won't be correct anymore, since it won't really "propagate an error"
  • When seeing a ? somewhere in a function without context, a viewer can't be sure it's related to error handling, although I feel a similar case can also be made for Option<..>, which does work with ?

If you don't need to inspect the error value, you can write this instead:

fn some_func() {
    let Ok(result) = something_fallible() else {
        println!("Handle the error here");
        return;
    };

    // .. do something with result
}
5 Likes

Yes, that's a nice pattern, but in this case I do want to inspect the error value.

Then what about:

fn some_func() {
    let Ok(result) = something_fallible().inspect_err(|err| {
        // .. do something with 'err'
    }) else {
        return;
    };

    // .. do something with result
}
4 Likes

This is already a lot better, and I will use this now, so thank you for the suggestion!

But, there are still some "problems" with it, for example the return statement is in a different code block than the .inspect_err statement. It's also more verbose to define the type of for generics, like:

let result: Type = something_fallible_generic()
  .map_err(..)?;

Which would become:

let Ok(result): Result<Type, _> = something_fallible_generic()
  .inspect_err(..)
else {
  return;
};

In most cases, the following could be an option, but that depends on the amount of generic parameters:

let Ok(result) = something_fallible_generic::<Type>()
  .inspect_err(..)
else {
  return;
};

In any case it's always a lot more verbose than a single ?, so I would still prefer to have that...

Personally, I'm waiting for postfix macros so I can make an unwrap_or_else macro like

let result = something_fallible().unwrap_or_else!(e => {
    println!("Handle the error here");
    return;
});

which I think strikes a very good balance between terse and clear.

7 Likes

I don't have a source to cite, but I'm fairly sure that Try for () has all but explicitly been rejected by the language and/or library teams.

The general "long shot maybe" for this use case is either something like a let-else-match, e.g.

let Ok(out) = try_make_out() else match {
    Err(e) => { /* diverges */ },
};

where the tail match doesn't have to cover the pattern used in the let, or some form of postfix macros allowing you to write something like[1]

let out = try_make_out().unwrap_or_else! { e =>
    /* diverges */
};

and while neither seem likely (let-else-match edges out postfix macros because those are a rats nest of additional complexity) they are a known pain point.


I've occasionally run into a desire to bail early in this manner, but almost every case I've had so far (with Result or Option rather than a custom enum) was actually improved by having the function return Option<()> instead, indicating if the operation was a success in executing the whole routine, or if it had to bail out early. Even if all of your current usages are fire-and-forget, having that information available makes for a better API surface. (Then it becomes map_err(|e| handle(e)).ok()? which isn't that bad.)


  1. Making it Try agnostic is an interesting exercise:

    macro unwrap_or_else($self, $pat:pat => $($or_else:tt)*) {
        match Try::branch($self) {
            ControlFlow::Continue(output) => output,
            ControlFlow::Break(residual) => {
                let $pat = residual; // …uh
                // enforce divergence
                let false = true else { $($or_else)* };
            }
        }
    }
    

    except note that the residual of Result<T, E> is Result<!, E>, not just E, which is what distinguishes the different ? "flavors;" this'd need yet another Try adjacent trait for unpacking single-variant residuals, and Try + FromResidual is already quite involved of an API even before considering try blocks' added needs (to usefully constrain inference).

    And no, switching Try to carry the do yeet payload directly instead of a residual isn't an option, because a deliberate goal of the Try design is that it should be possible to write result.adapt()? to get the effect of match result { Ok(t) => return Ok(t), Err(e) => e }. ↩︎

2 Likes

You're looking for Implement `Try` and `FromResidual<!>` for `()` · Issue #187 · rust-lang/libs-team · GitHub

So the ?-on-() part seems pretty dead, but maybe there's some version of the FromResidual part that could one day make sense. I don't have high confidence in that, though.

2 Likes

I'm often using Option<Infallible>. It is a zst equivalent to (), but allowes ?. When never type will get stabilized, it would become pretty Option<!>

2 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.