A Guide to Language-Agnostic Error Handling With Error-Stack

Estimated read time 36 min read

Like most programming languages, Rust encourages the programmer to handle errors in a particular way. Generally speaking, error handling is divided into two broad categories: exceptions and return values. Rust opts for return values. 

In this article, we intend to provide a comprehensive treatment of how to deal with errors in Rust. More than that, we will attempt to introduce error handling one piece at a time so that you’ll come away with a solid working knowledge of how everything fits together. 

When done naively, error handling in Rust can be verbose and annoying. This section will explore those stumbling blocks and demonstrate how to use the standard library to make error handling concise and ergonomic

The Basics

You can think of error handling as using case analysis to determine whether a computation was successful or not. as you will see, the key to ergonomic error handling is reducing the amount of explicit case analysis the programmer has to do while keeping code composable. 

Keeping code composable is important, because, without that requirement, we could panic whenever we come across something unexpected. ( panic causes the current task to unwind, and in most cases, the entire program aborts).

Here’s an example: 

// Guess a number between 1 and 10.

// If it matches the number we had in mind, return `true`. Else, return `false`

fn guess(n: i32) -> bool { 

if n < 1 || n > 10 { 

panic!(“Invalid number: {}”, n); 

} 

n == 5 

} 

fn main() { 

guess(11); 

} 

If you try running this code, the program will crash with a message like this: 

thread ‘main’ panicked at ‘Invalid number: 11’,src/bin/panic-simple.rs:5 

Here’s another example that is slightly less contrived. a program that accepts an integer as an argument, doubles it, and prints it. 

use std::env; 

fn main() { 

let mut argv = env::args(); 

let arg: String = argv.nth(1).unwrap();

// error 1

let n: i32 = arg.parse().unwrap();

// error 2

println!(“{}”, 2 * n); 

} 

 

If you give this program zero arguments (error 1) or if the first argument isn’t an integer (error 2), the program will panic just like in the first example. 

You can think of this style of error handling as similar to a bull running through a china shop. The bull will get to where it wants to go, but it will trample everything in the process. 

Unwrapping Explained

In the previous example, we claimed that the program would simply panic if it reached one of the two error conditions, yet, the program does not include an explicit call to panic like in the first example. This is because panic is embedded in the calls to unwrap. 

To “unwrap” something in Rust is to say, “Give me the result of the computation, and if there was an error, panic and stop the program.” It would be better if we showed the code for unwrapping because it is so simple, but to do that, we will first need to explore the option and Result types. Both of these types have a method called unwrap defined on them. 

The option Type

The option type is defined in the standard library: 

enum option<T> { 

None, 

Some(T), 

} 

The option type is a way to use Rust’s type system to express the possibility of absence. Encoding the possibility of absence into the type system is an important concept because it will cause the compiler to force the programmer to handle that absence.

Let’s take a look at an example that tries to find a character in a string: 

  

// Searches `haystack` for the Unicode character `needle`.

If one is found, the // byte offset of the character is returned.

otherwise, `None` is returned.

fn find(haystack: &str, needle: char) -> option<usize> { 

for (offset, c) in haystack.char_indices() { 

if c == needle { 

return Some(offset); 

} 

} 

None 

}

Notice that when this function finds a matching character, it doesn’t only return the offset. Instead, it returns Some(offset). Some is a variant or a value constructor for the option type. You can think of it as a function with the type fn<T>(value: T) -> option<T>. correspondingly, None is also a value constructor, except it has no arguments. You can think of None as a function with the type fn<T>() -> option<T>

This might seem like much ado about nothing, but this is only half of the story. The other half is using the find function we’ve written. Let’s try to use it to find the extension in a file name. 

fn main() { 

let file_name = “foobar.rs”; 

match find(file_name, ‘.’) { 

None => println!(“No file extension found.”), 

Some(i) => println!(“File extension: {}”, &file_name[i+1..]), } 

} 

This code uses pattern matching to do case analysis on the option<usize> returned by the find function. In fact, case analysis is the only way to get at the value stored inside an option<T>. This means that you, as the programmer, must handle the case when an option<T> is None instead of Some(t).

But wait, what about unwrap, which we used previously? There was no case analysis there! Instead, the case analysis was put inside the unwrap method for you. You could define it yourself if you want: 

  

enum option<T> { 

None, 

Some(T), 

} 

impl<T> option<T> { 

fn unwrap(self) -> T { 

match self { 

option::Some(val) => val, 

option::None => 

panic!(“called `option::unwrap()` on a `None` value”), 

} 

} 

}

The unwrap method abstracts away the case analysis. This is precisely the thing that makes unwrap ergonomic to use. Unfortunately, that panic! means that unwrap is not composable: it is the bull in the china shop. 

Composing Option<T> Values

In an example from before, we saw how to use find to discover the extension in a file name. of course, not all file names have a. in them, so it’s possible that the file name has no extension. This possibility of absence is encoded into the types using option<T>. In other words, the compiler will force us to address the possibility that an extension does not exist. In our case, we only print out a message saying such. 

Getting the extension of a file name is a pretty common operation, so it makes sense to put it into a function: 

   

// Returns the extension of the given file name, where the extension is defined // as all characters following the first `.`. 

// If `file_name` has no `.`, then `None` is returned. 


fn extension_explicit(file_name: &str) -> option<&str> { 

match find(file_name, ‘.’) { 

None => None, 

Some(i) => Some(&file_name[i+1..]), 

} 

}

(pro-tip: don’t use this code. Use the extension method in the standard library instead.) 

The code stays simple, but the important thing to notice is that the type of find forces us to consider the possibility of absence. This is a good thing because it means the compiler won’t let us accidentally forget about the case where a file name doesn’t have an extension. on the other hand, doing explicit case analysis like we’ve done in extension_explicit every time can get a bit tiresome. 

In fact, the case analysis in extension_explicit follows a very common pattern: map a function on to the value inside of an option<T>, unless the option is None, in which case, return None. 

Rust has parametric polymorphism, so it is very easy to define a combinator that abstracts this pattern: 

  

fn map<F, T, a>(option: option<T>, f: F) -> option<a> where F: Fnonce(T) -> a { match option { 

None => None, 

Some(value) => Some(f(value)), 

} 

} 

Indeed, the map is defined as a method on option<T> in the standard library. as a method, it has a slightly different signature: methods take self, &self, or &mut self as their first argument. 

armed with our new combinator, we can rewrite our extension_explicit method to get rid of the case analysis: 

// Returns the extension of the given file name, where the extension is defined // as all characters following the first `.`. 

// If `file_name` has no `.`, then `None` is returned. 

fn extension(file_name: &str) -> option<&str> { 

find(file_name, ‘.’).map(|i| &file_name[i+1..]) 

} 

One other pattern we commonly find is assigning a default value to the case when an option value is None. For example, maybe your program assumes that the extension of a file is rs even if none is present.

As you might imagine, the case analysis for this is not specific to file extensions – it can work with any option<T>:

fn unwrap_or<T>(option: option<T>, default: T) -> T { 

match option { 

None => default, 

Some(value) => value, 

} 

} 

Like with the map above, the standard library implementation is a method instead of a free function. 

The trick here is that the default value must have the same type as the value that might be inside the option<T>. Using it is dead simple in our case: 

fn main() { 

assert_eq!(extension(“foobar.csv”).unwrap_or(“rs”), “csv”); assert_eq!(extension(“foobar”).unwrap_or(“rs”), “rs”); } 

  

(Note that unwrap_or is defined as a method on option<T> in the standard library, so we use that here instead of the free-standing function we defined above. Don’t forget to check out the more general unwrap_or_else method.) 

There is one more combinator that we think is worth paying special attention to: and_then. It makes it easy to compose distinct computations that admit the possibility of absence. For example, much of the code in this section is about finding an extension given a file name. In order to do this, you first need the file name which is typically extracted from a file path. While most file paths have a file name, not all of them do.

For example, . , .. or / . 

So, we are tasked with the challenge of finding an extension given a file path.

Let’s start with explicit case analysis: 

fn file_path_ext_explicit(file_path: &str) -> option<&str> { 

match file_name(file_path) { 

None => None, 

Some(name) => match extension(name) { 

None => None, 

Some(ext) => Some(ext), 

} 

} 

} 

fn file_name(file_path: &str) -> option<&str> { 

// Implementation elided. 

unimplemented!() 

}

You might think that we could use the map combinator to reduce the case analysis, but its type doesn’t quite fit… 

fn file_path_ext(file_path: &str) -> option<&str>

{ 

file_name(file_path).map(|x| extension(x))

// This causes a compilation error.

} 

The map function here wraps the value returned by the extension function inside an option<_> and since the extension function itself returns an option<&str> the expression file_name(file_path).map(|x| extension(x)) actually returns an option<option<&str>>

But since file_path_ext just returns option<&str> (and not option<option<&str>>) we get a compilation error. 

The result of the function taken by map as input is always rewrapped with Some. Instead, we need something like map, but which allows the caller to return a option<_> directly without wrapping it in another option<_>

Its generic implementation is even simpler than map: 

fn and_then<F, T, a>(option: option<T>, f: F) -> option<a> 

where F: Fnonce(T) -> option<a> { 

match option { 

None => None, 

Some(value) => f(value), 

} 

}

Now we can rewrite our file_path_ext function without explicit case analysis:

fn file_path_ext(file_path: &str) -> option<&str> { 

file_name(file_path).and_then(extension) 

} 

Side Note: Since and_then essentially works like map but returns an option<_> instead of an option<option<_>> it is known as flatmap in some other languages. 

The option type has many other combinators defined in the standard library. It is a good idea to skim this list and familiarize yourself with what’s available—they can often reduce case analysis for you. Familiarizing yourself with these combinators will pay dividends because many of them are also defined (with similar semantics) for Result, which we will talk about next. 

Combinators make using types like option ergonomic because they reduce explicit case analysis. They are also composable because they permit the caller to handle the possibility of absence in their own way.

Methods like unwrap remove choices because they will panic if option<T> is None

The Result Type

The Result type is also defined in the standard library: 

enum Result<T, E> { 

ok(T), 

Err(E), 

} 

The Result type is a richer version of option. Instead of expressing the possibility of absence like option does, Result expresses the possibility of error. Usually, the error is used to explain why the execution of some computation failed. This is a strictly more general form of option. consider the following type alias, which is semantically equivalent to the real option<T> in every way: 

type option<T> = Result<T, ()>; 

This fixes the second type parameter of Result to always be () (pronounced “unit” or “empty tuple”). Exactly one value inhabits the () type: () . (Yup, the type, and value level terms have the same notation!) 

The Result type is a way of representing one of two possible outcomes in a computation. By convention, one outcome is meant to be expected or “ ok ” while the other outcome is meant to be unexpected or “Err”

Just like option, the Result type also has an unwrap method defined in the standard library.

Let’s define it: 

impl<T, E: ::std::fmt::Debug> Result<T, E> { 

fn unwrap(self) -> T { 

match self { 

Result::ok(val) => val, 

Result::Err(err) => 

panic!(“called `Result::unwrap()` on an `Err` value: {:?}”, err), } 

} 

}

This is effectively the same as our definition for option::unwrap, except it includes the error value in the panic! message. This makes debugging easier, but it also requires us to add a Debug constraint on the E type parameter (which represents our error type). Since the vast majority of types should satisfy the Debug constraint, this tends to work out in practice. ( Debug on a type simply means that there’s a reasonable way to print a human-readable description of values with that type.) 

Okay, let’s move on to an example. 

Parsing Integers

The Rust standard library makes converting strings to integers dead simple. It’s so easy in fact, that it is very tempting to write something like the following: 

fn double_number(number_str: &str) -> i32 { 2 * number_str.parse::<i32>().unwrap() } 

fn main() { 

let n: i32 = double_number(“10”); assert_eq!(n, 20); 

} 

  

at this point, you should be skeptical of calling unwrap. For example, if the string doesn’t parse as a number, you’ll get a panic:

thread ‘main’ panicked at ‘called `Result::unwrap()` on an `Err` value: parseIntError { kind: InvalidDigit }’, /home/rustbuild/src/rust buildbot/slave/beta-dist-rustc-linux/build/src/libcore/result.rs:729

This is rather unsightly, and if this happened inside a library you’re using, you might be understandably annoyed. Instead, we should try to handle the error in our function and let the caller decide what to do. This means changing the return type of double_number. But to what? Well, that requires looking at the signature of the parse method in the standard library: 

 

impl str { 

fn parse<F: FromStr>(&self) -> Result<F, F::Err>; 

} 

So we at least know that we need to use a Result. certainly, it’s possible that this could have returned an option. after all, a string either parses as a number or it doesn’t, right? That’s certainly a reasonable way to go, but the implementation internally distinguishes why the string didn’t parse as an integer. (Whether it’s an empty string, an invalid digit, too big, or too small.)

Therefore, using a Result makes sense because we want to provide more information than simply “absence.” We want to say why the parsing failed. You should try to emulate this line of reasoning when faced with a choice between option and Result.

If you can provide detailed error information, then you probably should. (We’ll see more on this later.) 

Okay, but how do we write our return type? The parse method as defined above is generic over all the different number types defined in the standard library. We could (and probably should) also make our function generic, but let’s favor explicitness for the moment.

We only care about i32, so we need to find its implementation of FromStr (do a CTRL-F in your browser for “FromStr”) and look at its associated type Err.

We did this so we can find the concrete error type. In this case, it’s std::num::parseIntError.

Finally, we can rewrite our function: 

use std::num::parseIntError; 

fn double_number(number_str: &str) -> Result<i32, parseIntError> { match number_str.parse::<i32>() { 

ok(n) => ok(2 * n), 

Err(err) => Err(err), 

} 

} 

fn main() { 

match double_number(“10”) { 

ok(n) => assert_eq!(n, 20), 

Err(err) => println!(“Error: {:?}”, err), 

} 

}

This is a little better, but now we’ve written much more code! The case analysis has once again bitten us. 

Combinators to the rescue! Just like option, Result has lots of combinators defined as methods. There is a large intersection of common combinators between Result and option. 

In particular, map is part of that intersection: 

use std::num::parseIntError; 

fn double_number(number_str: &str) -> Result<i32, parseIntError> { number_str.parse::<i32>().map(|n| 2 * n) 

} 

fn main() { 

match double_number(“10”) { 

ok(n) => assert_eq!(n, 20), 

Err(err) => println!(“Error: {:?}”, err), 

} 

}

The usual suspects are all there for Result, including unwrap_or and and_then. additionally, since Result has a second type parameter, there are combinators that affect only the error type, such as map_err (instead of map) and or_else (instead of and_then). 

The Result Type Alias Idiom

In the standard library, you may frequently see types like Result<i32>. But wait, we defined Result to have two type parameters. How can we get away with only specifying one?

The key is to define a Result type alias that fixes one of the type parameters to a particular type. Usually, the fixed type is the error type.

For example, our previous example parsing integers could be rewritten like this: 

use std::num::parseIntError; 

use std::result; 

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

fn double_number(number_str: &str) -> Result<i32> { 

unimplemented!(); 

} 

Why would we do this? Well, if we have a lot of functions that could return parseIntError, then it’s much more convenient to define an alias that always uses parseIntError so that we don’t have to write it out all the time. 

The most prominent place this idiom is used in the standard library is with io::Result. Typically, one writes io::Result<T>, which makes it clear that you’re using the io module’s type alias instead of the plain definition from std::result. (This idiom is also used for fmt::Result.) 

Working With Multiple Error Types

Thus far, we’ve looked at error handling where everything was either an option<T> or a Result<T, SomeError>. But what happens when you have both an option and a Result ? or what if you have a Result<T, Error1> and a Result<T, Error2>? Handling the composition of distinct error types is the next challenge in front of us, and it will be the major theme throughout the rest of this section. 

Composing Option and Result

So far, I’ve talked about combinators defined for option and combinators defined for Result. We can use these combinators to compose the results of different computations without doing explicit case analysis. 

Of course, in real code, things aren’t always as clean. Sometimes you have a mix of an option and Result types. Must we resort to explicit case analysis, or can we continue using combinators? 

For now, let’s revisit one of the first examples in this section: 

use std::env; 

fn main() { 

let mut argv = env::args(); 

let arg: String = argv.nth(1).unwrap();

// error 1

let n: i32 = arg.parse().unwrap();

// error 2

println!(“{}”, 2 * n); 

} 

 

Given our new-found knowledge of option, Result, and their various combinators, we should try to rewrite this so that errors are handled properly and the program doesn’t panic if there’s an error. 

The tricky aspect here is that argv.nth(1) produces an option while arg.parse() produces a Result. These aren’t directly composable. When faced with both an option and a Result, the solution is usually to convert the option to a Result. In our case, the absence of a command line parameter (from env::args() ) means the user didn’t invoke the program correctly. We could use a String to describe the error.

Let’s try: 

use std::env; 

fn double_arg(mut argv: env::args) -> Result<i32, String> { argv.nth(1) 

.ok_or(“please give at least one argument”.to_owned()) 

.and_then(|arg| arg.parse::<i32>().map_err(|err| err.to_string())) .map(|n| 2 * n) 

} 

fn main() { 

match double_arg(env::args()) { 

ok(n) => println!(“{}”, n), 

Err(err) => println!(“Error: {}”, err), 

} 

}

There are a couple of new things in this example. The first is the use of the option::ok_or combinator.

This is one way to convert an option into a Result. The conversion requires you to specify what error to use if option is None. Like the other combinators we’ve seen, its definition is very simple: 

fn ok_or<T, E>(option: option<T>, err: E) -> Result<T, E> { 

match option { 

Some(val) => ok(val), 

None => Err(err), 

} 

} 

The other new combinator used here is Result::map_err. This is like Result::map, except it maps a function on to the error portion of a Result value. If the Result is an ok(…) value, then it is returned unmodified. 

We use map_err here because it is necessary for the error types to remain the same (because of our use of and_then). Since we chose to convert the option<String> (from argv.nth(1) ) to a Result<String, String>, we must also convert the parseIntError from arg.parse() to a String.

The Try! Macro

A cornerstone of error handling in Rust is the try! macro. The try! macro abstracts case analysis like combinators, but unlike combinators, it also abstracts control flow. Namely, it can abstract the early return pattern seen above. 

Here is a simplified definition of a try! macro

macro_rules! try { 

($e:expr) => (match $e { 

ok(val) => val, 

Err(err) => return Err(err), 

}); 

} 

(The real definition is a bit more sophisticated. We will address that later.) 

Using the try! macro makes it very easy to simplify our last example. Since it does the case analysis and the early return for us, we get tighter code that is easier to read: 

use std::fs::File; 

use std::io::Read; 

use std::path::path; 

fn file_double<p: asRef<path>>(file_path: p) -> Result<i32, String> { let mut file = try!(File::open(file_path).map_err(|e| e.to_string())); let mut contents = String::new(); 

try!(file.read_to_string(&mut contents).map_err(|e| e.to_string())); let n = try!(contents.trim().parse::<i32>().map_err(|e| e.to_string())); ok(2 * n) 

} 

fn main() { 

match file_double(“foobar”) { 

ok(n) => println!(“{}”, n), 

Err(err) => println!(“Error: {}”, err), 

} 

}

The map_err calls are still necessary given our definition of try!. This is because the error types still need to be converted to String. The good news is that we will soon learn how to remove those map_err calls! The bad news is that we will need to learn a bit more about a couple of important traits in the standard library before we can remove the map_err calls. 

Defining Your Own Error Type

Before we dive into some of the standard library error traits, I’d like to wrap up this section by removing the use of String as our error type in the previous examples. 

Using String as we did in our previous examples is convenient because it’s easy to convert errors to strings, or even make up your own errors as strings on the spot. However, using String for your errors has some downsides. 

The first downside is that the error messages tend to clutter your code. It’s possible to define the error messages elsewhere, but unless you’re unusually disciplined, it is very tempting to embed the error message into your code. Indeed, we did exactly this in a previous example. 

The second and more important downside is that String S is lossy. That is, if all errors are converted to strings, then the errors we pass to the caller become completely opaque. The only reasonable thing the caller can do with a String error shows it to the user. certainly, inspecting the string to determine the type of error is not robust. (admittedly, this downside is far more important inside of a library as opposed to, say, an application.) 

For example, the io::Error type embeds an io::ErrorKind, which is structured data that represents what went wrong during an Io operation. This is important because you might want to react differently depending on the error. (e.g., a Brokenpipe error might mean quitting your program gracefully while a NotFound error might mean exiting with an error code and showing an error to the user.)

With io::ErrorKind, the caller can examine the type of an error with case analysis, which is strictly superior to trying to tease out the details of an error inside of a String

Instead of using a String as an error type in our previous example of reading an integer from a file, we can define our own error type that represents errors with structured data. We endeavor to not drop information from underlying errors in case the caller wants to inspect the details. 

The ideal way to represent one of many possibilities is to define our own sum type using enum. In our case, an error is either an io::Error or a num::parseIntError, so a natural definition arises: 

use std::io; 

use std::num; 

// We derive `Debug` because all types should probably derive `Debug`.


// This gives us a reasonable human-readable description of `cliError` values.


#[derive(Debug)] 

enum cliError { 

Io(io::Error), 

parse(num::parseIntError), 

}

Tweaking our code is very easy. Instead of converting errors to strings, we simply convert them to our cliError type using the corresponding value constructor: 

use std::fs::File; 

use std::io::Read; 

use std::path::path; 

fn file_double<p: asRef<path>>(file_path: p) -> Result<i32, cliError> { let mut file = try!(File::open(file_path).map_err(cliError::Io)); let mut contents = String::new(); 

try!(file.read_to_string(&mut contents).map_err(cliError::Io)); let n: i32 = try!(contents.trim().parse().map_err(cliError::parse)); ok(2 * n) 

} 

fn main() { 

match file_double(“foobar”) { 

ok(n) => println!(“{}”, n), 

Err(err) => println!(“Error: {:?}”, err), 

} 

} 

  

The only change here is switching map_err(|e| e.to_string()) (which converts errors to strings) to map_err(cliError::Io) or map_err(cliError::parse). The caller gets to decide the level of detail to report to the user. In effect, using a String as an error type removes choices from the caller while using a custom enum error type like cliError gives the caller all of the conveniences as before in addition to structured data describing the error. 

A rule of thumb is to define your own error type, but a String error type will do in a pinch, particularly if you’re writing an application. If you’re writing a library, defining your own error type should be strongly preferred so that you don’t remove choices from the caller unnecessarily. 

Standard Library Traits Used For Error Handling

The standard library defines two integral traits for error handling: std::error::Error and std::convert::From. While Error is designed specifically for generically describing errors, the From trait serves a more general role in converting values between two distinct types. 

The Error Trait

The Error trait is defined in the standard library: 

   

use std::fmt::{Debug, Display}; 

trait Error: Debug + Display { 

/// a short description of the error. 

fn description(&self) -> &str; 

/// The lower level cause of this error, if any. 

fn cause(&self) -> option<&Error> { None } 

}

This trait is super generic because it is meant to be implemented for all types that represent errors. This will prove useful for writing composable code as we’ll see later. otherwise, the trait allows you to do at least the following things: 

  • Obtain a Debug representation of the error. 
  • Obtain a user-facing Display representation of the error. 
  • Inspect the causal chain of an error, if one exists (via the cause method). 
  • Obtain a short description of the error (via the description method). 

The first two are a result of an Error requiring impls for both Debug and Display. The latter two are from the two methods defined on Error. The power of Error comes from the fact that all error types impl Error, which means errors can be existentially quantified as a trait object. This manifests as either Box<Error> or &Error.

Indeed, the cause method returns an &Error, which is itself a trait object. We’ll revisit the Error trait’s utility as a trait object later. 

For now, it suffices to show an example of implementing the Error trait.

Let’s use the error type we defined in the previous section: 

use std::io; 

use std::num; 

// We derive `Debug` because all types should probably derive `Debug`.

// This gives us a reasonable human-readable description of `cliError` values.

#[derive(Debug)] 

enum cliError { 

Io(io::Error), 

parse(num::parseIntError), 

}

This particular error type represents the possibility of two types of errors occurring: an error dealing with I/o or an error converting a string to a number. The error could represent as many error types as you want by adding new variants to the enum definition. 

Implementing Error is pretty straightforward. It’s mostly going to be a lot of explicit case analysis. 

 

use std::error; 

use std::fmt; 

impl fmt::Display for cliError { 

fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 

match *self { 

// Both underlying errors already impl `Display`, so we defer to // their implementations. 

cliError::Io(ref err) => write!(f, “Io error: {}”, err), 

cliError::parse(ref err) => write!(f, “parse error: {}”, err), 

} 

} 

} 

impl error::Error for cliError { 

fn description(&self) -> &str { 

// Both underlying errors already impl `Error`, so we defer to their // implementations. 


match *self { 

cliError::Io(ref err) => err.description(), 

cliError::parse(ref err) => err.description(), 

} 

} 

fn cause(&self) -> option<&error::Error> { 

match *self { 

// N.B. Both of these implicitly cast `err` from their concrete // types (either `&io::Error` or `&num::parseIntError`) 

// to a trait object `&Error`. This works because both error types // implement `Error`. 

cliError::Io(ref err) => Some(err), 

cliError::parse(ref err) => Some(err), 

} 

} 

}

We note that this is a very typical implementation of Error:match your different error types and satisfy the contracts defined for description and cause. 

The From Trait

The std::convert::From trait is defined in the standard library: 

trait From<T> { 

fn from(T) -> Self; 

} 

Deliciously simple, yes? From its very useful because it gives us a generic way to talk about conversion from a particular type T to some other type (in this case, “some other type” is the subject of the impl, or Self). The crux of From is the set of implementations provided by the standard library. 

Here are a few simple examples demonstrating how From works: 

let string: String = From::from(“foo”); 

let bytes: Vec<u8> = From::from(“foo”); 

let cow: ::std::borrow::cow<str> = From::from(“foo”); 

Okay, so From is useful for converting between strings. But what about errors? It turns out, there are one critical impl: 

impl<'a, E: Error + 'a> From<E> for Box<Error + 'a> 

This impl says that for any type that impls Error, we can convert it to a trait object Box<Error>. This may not seem terribly surprising, but it is useful in a generic context. 

Remember the two errors we were dealing with previously? Specifically, io::Error and num::parseIntError. Since both impl Error, they work with From

use std::error::Error; 

use std::fs; 

use std::io; 

use std::num; 

// We have to jump through some hoops to actually get error values: let io_err: io::Error = io::Error::last_os_error(); 

let parse_err: num::parseIntError = “not a number”.parse::<i32>().unwrap_err(); 

// oK, here are the conversions: 

let err1: Box<Error> = From::from(io_err); 

let err2: Box<Error> = From::from(parse_err); 

There is a really important pattern to recognize here. Both err1 and err2 have the same type. This is because they are existentially quantified types, or trait objects. In particular, their underlying type is erased from the compiler’s knowledge, so it truly sees err1 and err2 as exactly the same. additionally, we constructed err1 and err2 using precisely the same function call: From::from. This is because From::from is overloaded on both its argument and its return type. 

This pattern is important because it solves a problem we had earlier: it gives us a way to reliably convert errors to the same type using the same function. 

Time to revisit an old friend; the try! macro.

The Real Try! Macro

Previously, we presented this definition of try!

macro_rules! try { 

($e:expr) => (match $e { 

ok(val) => val, 

Err(err) => return Err(err), 

}); 

} 

This is not its real definition. Its real definition is in the standard library

macro_rules! try { 

($e:expr) => (match $e { 

ok(val) => val, 

Err(err) => return Err(::std::convert::From::from(err)), }); 

} 

      

There’s one tiny but powerful change: the error value is passed through From::from. This makes the try! macro much more powerful because it gives you automatic type conversion for free. 

Armed with our more powerful try! macro, let’s take a look at code we wrote previously to read a file and convert its contents to an integer: 

use std::fs::File; 

use std::io::Read; 

use std::path::path; 

fn file_double<p: asRef<path>>(file_path: p) -> Result<i32, String> { let mut file = try!(File::open(file_path).map_err(|e| e.to_string())); let mut contents = String::new(); 

try!(file.read_to_string(&mut contents).map_err(|e| e.to_string())); let n = try!(contents.trim().parse::<i32>().map_err(|e| e.to_string())); ok(2 * n) 

} 

Earlier, we promised that we could get rid of the map_err calls. Indeed, all we have to do is pick a type that From works with. As we saw in the previous section, From has an impl that lets it 

Convert any error type into a Box<Error>

use std::error::Error; 

use std::fs::File; 

use std::io::Read; 

use std::path::path; 

fn file_double<p: asRef<path>>(file_path: p) -> Result<i32, Box<Error>> { let mut file = try!(File::open(file_path)); 

let mut contents = String::new(); 

try!(file.read_to_string(&mut contents)); 

let n = try!(contents.trim().parse::<i32>()); 

ok(2 * n) 

} 

 

Error Handling with Box<Error>

Box<Error> is nice because it just works. You don’t need to define your own error types and you don’t need any From implementations. The downside is that since Box<Error> is a trait object, it erases the type, which means the compiler can no longer reason about its underlying type. 

Previously we started refactoring our code by changing the type of our function from T to Result<T, ourErrorType>. In this case, ourErrorType is only Box<Error>. But what’s T? and can we add a return type to main? 

The answer to the second question is no, we can’t. That means we’ll need to write a new function. But what is T? The simplest thing we can do is to return a list of matching Row values as a Vec<Row>. (Better code would return an iterator, but that is left as an exercise to the reader.) 

Let’s refactor our code into its own function, but keep the calls to unwrap. Note that we opt to handle the possibility of a missing population count by simply ignoring that row. 

use std::path::path; 

struct Row { 

// This struct remains unchanged. 

} 

struct populationcount { 

city: String, 

country: String, 

// This is no longer an `option` because values of this type are only // constructed if they have a population count. 

count: u64, 

} 

fn print_usage(program: &str, opts: options) { 

println!(“{}”, opts.usage(&format!(“Usage: {} [options] <data-path> <city>”, program))); 

} 

fn search<p: asRef<path>>(file_path: p, city: &str) -> Vec<populationcount> { let mut found = vec![]; 

let file = File::open(file_path).unwrap(); 

let mut rdr = csv::Reader::from_reader(file); 

for row in rdr.decode::<Row>() { 

let row = row.unwrap(); 

match row.population { 

None => { } // Skip it. 

Some(count) => if row.city == city { 

found.push(populationcount { 

city: row.city, 

country: row.country, 

count: count, 

}); 

}, 

} 

} 

found 

} 

fn main() { 

let args: Vec<String> = env::args().collect(); 

let program = &args[0]; 

let mut opts = options::new(); 

opts.optflag(“h”, “help”, “Show this usage message.”); 

let matches = match opts.parse(&args[1..]) { 

ok(m) => { m } 

Err(e) => { panic!(e.to_string()) } 

}; 

if matches.opt_present(“h”) { 

print_usage(&program, opts); 

return; 

} 

let data_path = &matches.free[0]; 

let city: &str = &matches.free[1]; 

for pop in search(data_path, city) { 

println!(“{}, {}: {:?}”, pop.city, pop.country, pop.count); 

} 

}

While we got rid of one use of expect (which is a nicer variant of unwrap), we still should handle the absence of any search results. 

To convert this to proper error handling, we need to do the following;

1. change the return type of search to Result<Vec<populationcount>, Box<Error>>.

2. Use the try! macro so that errors are returned to the caller instead of panicking the program. 

3. Handle the error in the main

Let’s try it: 

 

use std::error::Error; 

// The rest of the code before this is unchanged. 

fn search<p: asRef<path>> 

(file_path: p, city: &str) 

-> Result<Vec<populationcount>, Box<Error>> { 

let mut found = vec![]; 

let file = try!(File::open(file_path)); 

let mut rdr = csv::Reader::from_reader(file); 

for row in rdr.decode::<Row>() { 

let row = try!(row); 

match row.population { 

None => { } // Skip it. 

Some(count) => if row.city == city { 

found.push(populationcount { 

city: row.city, 

country: row.country, 

count: count, 

}); 

}, 

} 

} 

if found.is_empty() { 

Err(From::from(“No matching cities with a population were found.”)) } else { 

ok(found) 

} 

}

Instead of x.unwrap(), we now have try!(x). Since our function returns a Result<T, E>, the try! macro will return early from the function if an error occurs.

At the end of the search we also convert a plain string to an error type by using the corresponding From impls: 

// We are making use of this impl in the code above, since we call `From::from` // on a `&’static str`. 

impl<‘a> From<&’a str> for Box<Error> 

// But this is also useful when you need to allocate a new string for an // error message, usually with `format!`. 

impl From<String> for Box<Error> 

Since the search now returns a Result<T, E>, the main should use case analysis when calling search : 

... 

match search(data_path, city) { 

ok(pops) => { 

for pop in pops { 

println!(“{}, {}: {:?}”, pop.city, pop.country, pop.count); 

} 

} 

Err(err) => println!(“{}”, err) 

} 

Hope you enjoy the article and found suitable information about Error handling with Error-Stack. For more tech blogs keep following us!

Sharing is Caring

You May Also Like

More From Author

+ There are no comments

Add yours