?
in main
and tests
Rust's error handling revolves around returning Result<T, E>
and using ?
to propagate errors. For those who write many small programs and, hopefully,
many tests, one common paper cut has been mixing entry points such as main
and #[test]
s with error handling.
As an example, you might have tried to write:
use std::fs::File;
fn main() {
let f = File::open("bar.txt")?;
}
Since ?
works by propagating the Result
with an early return to the
enclosing function, the snippet above does not work, and results today
in the following error:
error[E0277]: the `?` operator can only be used in a function that returns `Result`
or `Option` (or another type that implements `std::ops::Try`)
--> src/main.rs:5:13
|
5 | let f = File::open("bar.txt")?;
| ^^^^^^^^^^^^^^^^^^^^^^ cannot use the `?` operator in a function that returns `()`
|
= help: the trait `std::ops::Try` is not implemented for `()`
= note: required by `std::ops::Try::from_error`
To solve this problem in Rust 2015, you might have written something like:
// Rust 2015 # use std::process; # use std::error::Error; fn run() -> Result<(), Box<Error>> { // real logic.. Ok(()) } fn main() { if let Err(e) = run() { println!("Application error: {}", e); process::exit(1); } }
However, in this case, the run
function has all the interesting logic and
main
is just boilerplate. The problem is even worse for #[test]
s, since
there tend to be a lot more of them.
In Rust 2018 you can instead let your #[test]
s and main
functions return
a Result
:
// Rust 2018 use std::fs::File; fn main() -> Result<(), std::io::Error> { let f = File::open("bar.txt")?; Ok(()) }
In this case, if say the file doesn't exist and there is an Err(err)
somewhere,
then main
will exit with an error code (not 0
) and print out a Debug
representation of err
.
More details
Getting -> Result<..>
to work in the context of main
and #[test]
s is not
magic. It is all backed up by a Termination
trait which all valid return
types of main
and testing functions must implement. The trait is defined as:
# #![allow(unused_variables)] #fn main() { pub trait Termination { fn report(self) -> i32; } #}
When setting up the entry point for your application, the compiler will use this
trait and call .report()
on the Result
of the main
function you have written.
Two simplified example implementations of this trait for Result
and ()
are:
# #![allow(unused_variables)] #fn main() { # #![feature(process_exitcode_placeholder, termination_trait_lib)] # use std::process::ExitCode; # use std::fmt; # # pub trait Termination { fn report(self) -> i32; } impl Termination for () { fn report(self) -> i32 { # use std::process::Termination; ExitCode::SUCCESS.report() } } impl<E: fmt::Debug> Termination for Result<(), E> { fn report(self) -> i32 { match self { Ok(()) => ().report(), Err(err) => { eprintln!("Error: {:?}", err); # use std::process::Termination; ExitCode::FAILURE.report() } } } } #}
As you can see in the case of ()
, a success code is simply returned.
In the case of Result
, the success case delegates to the implementation for
()
but prints out an error message and a failure exit code on Err(..)
.
To learn more about the finer details, consult either the tracking issue or the RFC.