How it works...

The purpose of our example is to read a file, age.txt, and return the number written in it, assuming that it represents some kind of age. We can encounter three errors during this process:

  • Failure to read the file (maybe it doesn't exist)
  • Failure to read its content as a number (it could contain text as well)
  • The number could be negative

These possible error states are the possible variants of our Error enum: AgeReaderError[7]. It is usual to name the variants after the sub-errors they represent. Because a failure to read the file raises an io::Error, we name our corresponding variant AgeReaderError::Io[8]. A failure to parse a &str as an i32 raises a num::ParseIntError, so we name our encompassing variant AgeReaderError::Parse[9].

These two std errors show the naming convention of errors neatly. If you have many different errors that can be returned by a module, export them via their full name, such as num::ParseIntError. If your module only returns one kind of Error, simply export it as Error, such as io::Error. We intentionally don't follow this convention in the recipe because the distinct name AgeReaderError makes it easier to talk about it. If this recipe was included, one for one, in a crate, we could achieve the conventional effect by exporting it as pub type Error = AgeReaderError;.

The next thing we create is an alias for our own Result[14]:

type Result<T> = result::Result<T, AgeReaderError>;

This is an extremely common pattern for your own errors, which makes working with them a charm, as we see in the return type of read_age[77]:

fn read_age(filename: &str) -> Result<i32> { ... }

Looks nice, doesn't it? In order to use our enum as an Error though, we need to implement it first [16]. The Error trait requires two things: a description[17], which is a short explanation of what went wrong, and a cause[27], which is simply a redirection to the underlying error, if any. You can (and should) provide a detailed description of the problem at hand by also implementing Display for your Error[37]. In all of these implementations, you should refer to the underlying error if possible, as with the following line [20]:

AgeReaderError::Io(ref err) => err.description()

The last thing you need to provide for a good own Error is a From implementation for every sub-error. In our case this would be From<io::Error>[49] and From<num::ParseIntError>[55]. This way, the try operator (?) will automatically convert the involved errors for us.

After implementing all necessary traits, you can return the custom Error from any function and unwrap values in it with the aforementioned operator. In this example, when checking the result of read_age, we didn't have to match the returned value. In a real main function, we would probably just call .expect("…") on it, but we matched the individual error variants anyway to show you how nicely you can react to different problems when using known error types [65 to 73]:

match result {
Ok(num) => println!("{} contains the age {}", FILENAME, num),
Err(AgeReaderError::Io(err)) => eprintln!("Failed to open the file
{}: {}", FILENAME, err),
Err(AgeReaderError::Parse(err)) => eprintln!(
"Failed to read the contents of {} as a number: {}",
FILENAME, err
),
Err(AgeReaderError::NegativeAge()) => eprintln!("The age in the file is negative"),
}
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.133.150.41