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 ?
fn some_func() {
let Ok(result) = something_fallible().inspect_err(|err| {
// .. do something with 'err'
}) else {
return;
};
// .. do something with result
}
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(..)?;
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.)
Making it Try agnostic is an interesting exercise:
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 }. ↩︎
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.