- Feature Name: throwing_functions
- Start Date: 2017-06-20
- RFC PR: (leave this empty)
- Rust Issue: (leave this empty)
Summary
Add an annotation to functions that they may “throw” an error type, which changes the function’s
return type to Result<T, E>
where T
is the functions return type and E
is the function’s
annotated error type. Also add a macro throw!
to facilitate early return of errors.
Motivation
Rust’s error handling is on the one hand very powerful, and values correctness and clarity, on the other hand it can be somewhat cumbersome and unergonomic in some cases. This is why error handling has been singled out as part of the ergonomics initiative.
The introduction of the ?
operator and the Try
trait has made it easier to write functions which propagate errors. With RFC1937, the ?
operator will become even more prevalent.
One downside with the ?
operator is that the positive case, when no error has occurred, still
necessitates special handling, since the return value needs to be wrapped in Ok()
. This is
especially cumbersome for functions which return ()
, since instead of simply not specifying
a return value, the unsightly Ok(())
needs to be returned. This is not only aesthetically
unfortunate, but also confusing to new users.
Here’s an example of the necessity of wrapping the return value in Ok
and the Ok(())
return
value in particular:
use std::path::Path;
use std::io::{self, Read};
use std::fs::File;
fn read_file(path: &Path) -> Result<String, io::Error> {
let mut buffer = String::new();
let file = File::open(path)?;
file.read_to_string(&mut buffer)?;
Ok(buffer)
}
fn main() -> Result<(), io::Error> {
let content = read_file(&Path::new("test.txt"))?;
println!("{}", content);
Ok(())
}
Detailed design
The syntax of function definitions, both for free functions, inherent impls, and functions in trait
definitions and implementations will be extended with the throws
contextual keyword and a type,
which must appear after the function’s return type and before any where
clauses.
The return type of any function which is annotated with throws
becomes Result<T, E>
where T
is the function’s return type and E
is the error type which appears after the throws
keyword.
The function body must evaluate to T
and any early return from the function must also evaluate
to T
. It is not possible to return a Result<T, E>
from the function body. If the ?
is used
within such a function, unless it is used within a catch
block, it will operate in the same way
as if it were used in a regular function with return type Result<T, E>
.
For example:
use std::path::Path;
use std::io::{self, Read};
use std::fs::File;
fn read_file(path: &Path) -> String throws io::Error {
let mut buffer = String::new();
let file = File::open(path)?;
file.read_to_string(&mut buffer)?;
buffer
}
fn main() throws io::Error {
let content = read_file(&Path::new("test.txt"))?;
println!("{}", content);
}
Forcing an error return
One particular case where this design is till somewhat cumbersome is in returning an error from
a function directly, this can be solved by adding a throw!($expr)
macro to the prelude, which expands to
Err($expr)?
. While it would be nicer to have a throw
keyword, similar to return
, since
throw
is currently not a reserved keyword, this would be a breaking change, and probably not
acceptable in Rust < 2.0.
For example,
fn main() throws MyError {
let content = read_file(&Path::new("test.txt"))?;
match &content {
"ok" => println!("Everything is ok!"),
_ => throw!(MyError::InvalidFileContent),
};
};
How We Teach This
This feature should be taught as part of Rust’s error handling story, in particular since it is closely tied
to the ?
operator, it should be taught alongside it. In particular the connection between this and the
Result
type should be made explicit. It is important to emphasize that this is not an exception mechanism
and that thrown errors do not automatically propagate up the callstack, like it might be expected from other
other languages, and as is the case with panics.
A major benefit of this feature is that in a pre-rigorous stage, users can be effective in handling errors in
Rust through the ?
operator, the throws
annotation and the throw!
macro, even if they are not familiar
with algebraic data types or Result
return types.
Drawbacks
The major downside of this feature is that it makes the function signature somewhat more opaque. It is not
immediately clear that the return value of the function is a Result
, in fact the Result
type does not
appear in the function signature at all. This can appear this feature appear to be somewhat magical. It may
hide the affect that other features of the Result
type, such as monadic combinators or its implementations
for FromIterator
may be used with throwing functions.
Alternatives
-
An alternative design would be to automatically coerce the return value of any function which returns a
Result
into aResult::Ok
value unless it is already a result. While this makes the type of the function more clear, it introduces a lot of ambiguity around the return value of the function. For example, if the return value of the function isResult<Result<T, E>, E>
, what would a return value ofErr(foo)
mean? -
Use a keyword for
throw
instead of a macro. While this would be aesthetically preferrable, it is probably not possible due to backward compatibility issues. -
Skip the
throw!
macro all together. This would mean that users would still have to learn about theErr
variant constructor in the pre-rigorous stage. -
Do nothing, which leaves the status quo of the somewhat unsightly
Ok(())
and friends.
Unresolved questions
-
What is the appropriate name for this feature? This has previously been called “checked exceptions”, but this terminology brings with it a lot of baggage, and also is not correct, since this is in fact not an exception mechanism.
-
Should this mechanism somehow be extended to other types which can be used with the
?
operator? -
This feature does not have a strightforward desugaring to Rust syntax, like the
?
operator does for example, are there any challenges with implementing this design?