This is an alternative design to Ok wrapping, which I am very excited about. Similar ideas have been floated in the past. Each subheading can be separated:
The throw
keyword
The throw keyword has the semantics:
throw $expr <=> return Err(From::from($expr))
This makes ?
sugar for:
match expr {
Ok(x) => x,
Err(e) => throw e,
}
The throw
keyword is added mainly for assisting in raising errors, replacing Err(e)?
expressions.
This integrates well with designs in which your errors may need to be into()
d, including error-chain (sometimes), failure (using Error
) and Box<Error>
from std.
Unlike return
, throw
is caught by catch
blocks.
throw
like ?
works with all Try
types (which should maybe renamed to Throw
, but that’s a separate discussion). For example, in functions that return options, throw NoneError;
catch E
@nikomatsakis has complained about type ascription issues with catch blocks as they currently exist. It would be great to allow inserting the error type into the catch expression directly:
// All `?`s and throws will convert into io::Error
catch io::Error {
function()?;
throw expr;
}
-> T catch E
First, catch
is fixed to have the semantics specified by the RFC - the final expression is wrapped in Ok
, rather than being required to be a result.
We add a special return syntax -> T catch E
, which is function that returns a Result<T, E>
. This like sugar for wrapping the body in a catch block, except that it catches returns as well:
fn foo() -> i32 catch Error {
if bar() {
// Ok wraps explicit returns
return 0;
} else if baz() {
// Err wraps throws
throw MyError;
}
// ? works
quux()?;
// Ok wraps final return
7
}
In functions where the Ok variant is ()
, you can just omit it, just like with functions that return ()
:
fn foo() catch io::Error {
writeln!(io::stderr(), "An error occurred.")?;
}
This is all syntactic sugar, the functions have the return type Result<T, E>
. Likely they should be normalized in rustdoc, etc.
Probably we can support other types that implement Throw
using the syntax -> T catch E as ThrowType
, for example, -> T catch NoneError as Option<T>
. It’s unclear to me how useful this is though. Or we could also allow omitting the E
in the as
case, because it can be inferred from the throw type (its an assoc type), so -> T catch as Option<T>
. Possibly this as
is a bit subtle.
Relationship to exceptions
What you get when you combine all these features is something that is very much like throwing exceptions in syntax, but with several key differences that, in my opinion, make it superior. In general, it preserves the ergonomics of throwing exceptions, while significantly improving your ability to reason locally about the paths of control flow.
The first critical difference is that every function call that could “throw” and return early is explicitly annotated with ?
. This allows users to see every possible point of error and early return in their function, avoiding the nonlocality and secret control flow problems that exceptions tend have.
More subtly, Result<T, E>
reifies the concept of fallibility. Languages with exceptions have types for their failures (the exception types), but not a type for the possibility of failure. This forces you to handle exceptions using only the built in try/catch control flow constructs. In contrast, Rust allows you to call methods on Result
, match over it normally, and just generally treat fallibility in itself as a value.
Both of these advantages are advantages we already have, but the point is that this proposal preserves these advantages while getting the nice ergonomics that have led most mainstream languages to primarily use exceptions for error handling.
Relationship to Ok-wrapping
I prefer this to just adding Ok wrapping, or possibly Ok wrapping ()
, because it becomes syntactically identified that this behavior is happening in the function declaration, rather than it being a special rule of the Result
type or something similar.