Question Mark

Lots of library functions--- including the standard library, as well as third-party libraries--- return either options or results, so you'll likely get used to using things like match, if let, while let, unwrap, and expect even before you make an Option or Result yourself.

Example: Open

For example, when we open a file in Perl, it returns an undefined value when it fails, so we usually write something like this

open my $fh, '<', $file or die "Cannot open file '$file': $!";

Upon success, our file handle is in $fh. Upon failure, we retrieve the reason for the failure from $!.

The similar maneuver in Rust

use std::fs::File;
use std::path::Path;
fn main() {
   let path = Path::new("no_such_file");
let result = File::open(&path);
   match result {
       Ok(f) => println!("The open succeeded\n{:#?}", f),
       Err(e) => println!("The open failed\n{:#?}", e),
   }
}

gives us a Result<File, Error>. Upon success, a std::fs::File is wrapped in an Ok. Among other things, it contains the file descriptor we need to read from the file. Upon failure, a std::io::Error is wrapped in an Err. Among other things, it contains the reason for the failure.

If we match on the result, we could inspect the File struct or the Error struct like so

use std::fs::File;
use std::path::Path;
fn main() {
   let path = Path::new("no_such_file");
   let result = File::open(&path);
match result {
    Ok(f) => println!("The open succeeded\n{:#?}", f),
    Err(e) => println!("The open failed\n{:#?}", e),
}
}

Again, we can call unwrap or expect on a Result and it will panic if it is Err, so perhaps a more direct analogue to

open my $fh, '<', $file or die "Cannot open file '$file': $!";

would be

use std::fs::File;
use std::path::Path;
fn main() {
   let path = Path::new("no_such_file");
let f = File::open(&path).expect("Cannot open file!");
   println!("The open succeeded\n{:#?}", f);
}

This unwraps our File on success and panics with our message on failure.

The ? (question mark) operator

Rust has a more idiomatic way of dealing with options and results. Perhaps this is best shown by example. Say we wanted to read the first eight bytes of a file (perhaps we want to check if it's a PNG file or something). We might write something like this

use std::fs::File;
use std::io;
use std::io::{Error, ErrorKind};
use std::io::prelude::*;
use std::path::Path;

fn main() {
   let path = Path::new("src/main.rs");

   let first_eight = read_eight_bytes(&path);

   dbg!(&first_eight);
}

fn read_eight_bytes(path: &Path) -> Result<[u8; 8], io::Error> {
    let result = File::open(path);

    let mut f = match result {
        Ok(f) => f,
        Err(e) => return Err(e),
    };

    let mut buffer = [0; 8];

    let result = f.read(&mut buffer[..]);

    let n = match result {
        Ok(n) => n,
        Err(e) => return Err(e),
    };

    if n == 8 {
        Ok(buffer)
    } else {
        Err(Error::new(ErrorKind::Other, "Could not read 8 bytes!"))
    }
}

We're given a path and we're returning a Result<[u8; 8], io::Error>. That is, we're going to return Ok with eight bytes or Err with the reason we couldn't.

First, we open the path, which might fail. If it does, we return its io::Error. If it succeeds, we unwrap the File and try to read eight bytes from it. If that fails, we return its io::Error. If it succeeds, we check that we got eight bytes. If so, return them in an Ok. If not, return a custom error.

Both of those matches have the same shape: unwrap the Ok or return early with the Err. This is so common, that we can replace it with a single question mark!

use std::fs::File;
use std::io;
use std::io::{Error, ErrorKind};
use std::io::prelude::*;
use std::path::Path;

fn main() {
   let path = Path::new("dirk-gently.png");
   let first_eight = read_eight_bytes(&path);
   dbg!(&first_eight);
}

fn read_eight_bytes(path: &Path) -> Result<[u8; 8], io::Error> {
    let mut f = File::open(path)?;

    let mut buffer = [0; 8];

    let n = f.read(&mut buffer[..])?;

    if n == 8 {
        Ok(buffer)
    } else {
        Err(Error::new(ErrorKind::Other, "Could not read 8 bytes!"))
    }
}

Not bad, eh? We're doing all the proper error checking, but it's mostly just the "happy path" showing in our code.

Also, Result<T, io::Error> is so common that there's a type alias for it, io::Result<T>. That is, we could replace


#![allow(unused_variables)]
fn main() {
fn read_eight_bytes(path: &Path) -> Result<[u8; 8], io::Error> {
}

with


#![allow(unused_variables)]
fn main() {
fn read_eight_bytes(path: &Path) -> io::Result<[u8; 8]> {
}

if we wanted.

Conclusion

It takes some getting used to perhaps, but Rust's error handling is really pretty nice. It may seem finicky, but at least we don't have to juggle undefined values or exceptions.