The ? operator for easier error handling
for Result<T, E>
for Option<T>
Rust has gained a new operator, ?
, that makes error handling more pleasantby reducing the visual noise involved. It does this by solving one simpleproblem. To illustrate, imagine we had some code to read some data from afile:
#![allow(unused_variables)]
fn main() {
use std::{io::{self, prelude::*}, fs::File};
fn read_username_from_file() -> Result<String, io::Error> {
let f = File::open("username.txt");
let mut f = match f {
Ok(file) => file,
Err(e) => return Err(e),
};
let mut s = String::new();
match f.read_to_string(&mut s) {
Ok(_) => Ok(s),
Err(e) => Err(e),
}
}
}
Note: this code could be made simpler with a single call to
std::fs::read_to_string
,but we're writing it all out manually here to have an example with multipleerrors.
This code has two paths that can fail, opening the file and reading the datafrom it. If either of these fail to work, we'd like to return an error fromread_username_from_file
. Doing so involves match
ing on the result of theI/O operations. In simple cases like this though, where we are onlypropagating errors up the call stack, the matching is just boilerplate -seeing it written out, in the same pattern every time, doesn't provide thereader with a great deal of useful information.
With ?
, the above code looks like this:
#![allow(unused_variables)]
fn main() {
use std::{io::{self, prelude::*}, fs::File};
fn read_username_from_file() -> Result<String, io::Error> {
let mut f = File::open("username.txt")?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}
}
The ?
is shorthand for the entire match statements we wrote earlier. Inother words, ?
applies to a Result
value, and if it was an Ok
, itunwraps it and gives the inner value. If it was an Err
, it returns from thefunction you're currently in. Visually, it is much more straightforward.Instead of an entire match statement, now we are just using the single "?"character to indicate that here we are handling errors in the standard way,by passing them up the call stack.
Seasoned Rustaceans may recognize that this is the same as the try!
macrothat's been available since Rust 1.0
. And indeed, they are the same.Previously, read_username_from_file
could have been implemented like this:
#![allow(unused_variables)]
fn main() {
use std::{io::{self, prelude::*}, fs::File};
fn read_username_from_file() -> Result<String, io::Error> {
let mut f = try!(File::open("username.txt"));
let mut s = String::new();
try!(f.read_to_string(&mut s));
Ok(s)
}
}
So why extend the language when we already have a macro? There are multiplereasons. First, try!
has proved to be extremely useful, and is used oftenin idiomatic Rust. It is used so often that we think it's worth having asweet syntax. This sort of evolution is one of the great advantages of apowerful macro system: speculative extensions to the language syntax can beprototyped and iterated on without modifying the language itself, and inreturn, macros that turn out to be especially useful can indicate missinglanguage features. This evolution, from try!
to ?
is a great example.
One of the reasons try!
needs a sweeter syntax is that it is quiteunattractive when multiple invocations of try!
are used in succession.Consider:
try!(try!(try!(foo()).bar()).baz())
as opposed to
foo()?.bar()?.baz()?
The first is quite difficult to scan visually, and each layer of errorhandling prefixes the expression with an additional call to try!
. Thisbrings undue attention to the trivial error propagation, obscuring the maincode path, in this example the calls to foo
, bar
and baz
. This sort ofmethod chaining with error handling occurs in situations like the builderpattern.
Finally, the dedicated syntax will make it easier in the future to producenicer error messages tailored specifically to ?
, whereas it is difficult toproduce nice errors for macro-expanded code generally.
You can use ?
with Result<T, E>
s, but also with Option<T>
. In thatcase, ?
will return a value for Some(T)
and return None
for None
. Onecurrent restriction is that you cannot use ?
for both in the same function,as the return type needs to match the type you use ?
on. In the future,this restriction will be lifted.