An alternative proposal to try
/catch
/throw
error handling
There is currently a lot of discussion going on about the future of Rust error handling. The general direction seems towards error handling that looks like it does in exception based languages.
Bias Warning: Personally, I’m opposed to such changes, and would like to build a counter-proposal.
First, some links to the current/previous discussions and posts:
- RFC: Reserve
try
fortry { .. }
block expressions - Tracking issue for
try
blocks - RFC:
throw
expressions - Trait-based exception handling
- Issue for Auto-Wrapping
A rough outline of the things that are in the pipeline or have been brought up in the past as possibilities would be:
-
try
/catch
blocks for catching and handling errors. -
throw
orfail
for raising errors to some catching scope. -
?
also raising towards some catching scope. - Auto-converting final expression results to
Ok
orSome
.
My goals with this proposal are the following:
- Users wishing to use these semantics can easily opt-in to them.
- Users who don’t want these semantics can still use the control flow semantics in an explicit manner.
- The additional semantics can be more powerful and feature-rich than language constructs, giving additional value to those that do want to use them.
- Unfamiliar users are still given hints that additional semantics might be in play.
Enhancing Rust to allow the ecosystem to provide the functionality
My proposal is: Instead of introducing special syntax for when error handling is close to that in exception based language, we build up from existing Rust control flow. This would allow all custom semantics to be built using macros. Given the existance of break-with-value, this would simply amount to adding a special block label.
This has a couple of advantages:
-
The semantics can be experimented with and iterated on by the ecosystem.
-
If such functionality is indeed wanted without extra dependencies at some point once things have settled, they can be included in
std
. -
It is much easier to make future adjustments and improvements to macros than it is to change language constructs. There is a massive difference between iterating core features, and iterating a library on crates.io, with the latter allowing a lot more freedom and flexibility.
-
Before the new semantics find wide adoption, there is an explicit opt-in with a dependency.
-
Even if the macros end up in
std
, the additional semantics are still being signalled by them being macro functionality. Rust developers are already trained to expect additional semantics from macros. -
The provided semantics can be a lot richer than is possible with builtin language syntax in the current context of error propagation. For example: There can be auto-converting/non-converting alternatives, allowing one to skip result type hints when the conversions aren’t necessary. Note: See this section of the "
throw
expression" proposal on a possible exception for propagating through scopes where types don’t match and the jump destination is determined by types. -
If some of the additional functionality turns out useful/wide spread enough to get it’s own syntax, it can still be done, but based on actual semantics of the usage in the wild. This is similar to how
try!
used to encapsulate a common use-case, and turned into?
once things had settled. -
It is easy for new macros that are more special cased to be introduced along-side the existing ones, while still remaining consistent.
-
It allows a bigger picture of error handling control flow requirements to develop in the community before anything is made part of the core language.
But there would also be advantages to users who opt not to use this kind of semantics:
-
The additional functionality provided by Rust used to implement the macros is also available to other parts of the ecosystem.
-
They are useful by itself even when not used by macros. As they allow early-escape from scopes in many scenarios, and are not limited to the success/error paradigm due to the lack of implicit auto-conversion.
-
The new functionality required to implement the macros in the ecosystem is very limited, so the core language surface doesn’t increase by much.
Introducing a built-in 'escape
block label
This is basically the only change that is required. There would be an
implicit 'escape
label at the function boundary, but developers can
also put it on a block inside the function to limit where ?
propagates
to.
In essence, this would simply introduce an explicitly specifiable
target block for ?
.
The usefulness of this would be provided by combining it with the break-with-value feature.
An interesting note is that the trait-based exception handling RFC that
introduced ?
actually uses break-with-value to explain
semantics.
This would also further minimize breakage, as label identifiers are a non-majority subset of used identifiers.
The label 'try
has come up as a possibility in the past. This
would also be a good possibility. A 'try
label also wouldn’t suffer
from too much exception association I believe, seeing how it looks
different than the construct in exception based languages.
Exception-like syntax as macros on top.
The new label would allow implementing the exception like syntax as macros on top:
-
A catching block macro with auto-conversion would use an
'escape
block that applies theTry
conversion to its result on the inside. It could include ways to give type-hints (which are often necessary with always-on conversions) or do error transforms as part of it’s syntax. It could also provide a non-converting alternative. Due to it being a macro it can be a lot more flexible with those features. -
A throwing macro can use
break 'escape Err(value)
. Conversion semantics could be applied as well, or converting and non-converting alternatives provided. This function would be able to throw to both a catching block and the function boundary.
Bike-shedding
Fortunately, there is very little of that. Just the name of the
'escape
block to reserve.
Examples of direct feature use
Limiting Error Propagation
let result: Result<_, MyError> = 'escape: {
let item_a = calc_a()?;
let item_b = calc_b()?;
Ok(combine(item_a, item_b)?)
};
Optional operations in sequences
let final: Option<_> = 'escape: {
let mut sum = 0;
for item in items {
sum += item.get_value()?;
}
Some(sum)
};
Searching for an item
let item = 'escape: {
for item in items {
let data = verify(item)?;
if matches(data) {
break 'escape Ok(data);
}
}
Err(MyError::NotFound)
};
Examples of implementable macros
A catching block with final-result conversion
macro_rules! catch {
($($body:tt)*) => {
'escape: { ::std::ops::Try::from_ok({ $($body)* }) }
}
}
let result = catch! { a? + b? };
An error throwing macro with final-result conversion
macro_rules! throw {
($value:expr) => {
break 'escape ::std::ops::Try::from_error($value)
}
}
fn open(path: &Path) -> Result<File, Error> {
match File::open(path) {
Ok(file) => Ok(file),
Err(io_err) => {
log("IO error occured");
throw!(io_error);
},
}
}
Finalising a block with a success value
macro_rules! final {
($value:expr) => {
break 'escape ::std::ops::Try::from_ok($value)
}
}
let value: Option<_> = catch! {
if let Some(cached) = cache.get(&id) {
final!(cached);
}
let new = calc();
cache.insert(id, new);
new
};
Other possibilities for macros
Note: The names aren’t final suggestions and merely serve as illustration. The list is also likely not exhaustive.
// catch without conversion
catch_plain! { ... }
// catch with preset Result<_, _> hint
fallible! { ... }
// catch with preset Option<_> hint
optional! { ... }
// catch with error as converted final result
attempt! { ... }
// catch with preset Result<_, Error>
catch_error!(Error { ... })
// catch with preset Result<Success, Failure>
catch_result!(Success or Failure { ... })
// catch with preset Option<Value>
catch_option!(Value { ... })
// throwing without conversion
catch_plain!(...)
// providing a final value without conversion
final_plain!(...)
// optionally throwing an Option<Error>
throw_if_some!(...)
// Finalizing an optional value
final_if_some!(...)
// Inline mapping of an error
handle!(do { ... } match err { ... })
// Inline mapping and hinting of an error
handle!(do { ... } match err: Error { ... })
// Special case for failure crate removing need to type hint errors
catch_failure! { ... }
Summary
I believe adding 'escape
functionality to blocks would allow us to pause on our way to try
/catch
/throw
syntax, and gain experience before things are finalized as core language syntax.
With this post I’m hoping to find out if there would be enough support for turning this into an RFC and considering it an alternative (for the immediate future, at least).