? 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 theenclosing function, the snippet above does not work, and results todayin 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 andmain
is just boilerplate. The problem is even worse for #[test]
s, sincethere tend to be a lot more of them.
In Rust 2018 you can instead let your #[test]
s and main
functions returna 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 notmagic. It is all backed up by a Termination
trait which all valid returntypes 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 thistrait 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.