Rust result error

Обработка ошибок и перечисление Result в языке программирования Rust

Последнее обновление: 04.05.2021

Многие ошибки не требуют завершения программы. Такие ошибки можно обработать и продолжить выполнение программы. Для этой цели в Rust
предназначен тип Result. Данный тип представляет enum или перечисление, которое определяет две константы:
Ok и Err.

enum Result<T, E> {
    Ok(T),
    Err(E),
}

Это перечисление типизировано двумя параметрами типа: T и E. T представляет тип
значения, который будет возвращаться в случае успешного выполнения. А параметр E представляет тип ошибки,
которая будет возвращаться через константу Err в случае возникновения ошибки.

Рассмотрим, как использовать перечисление Result. Допустим, у нас есть функция, которая создает объект некоторой структуры:

struct Person{ name: String, age:u8}

fn create_person(username: &str, userage: u8) -> Result<Person, String>{
	
	if userage < 110{
		let new_person = Person{name: String::from(username), age: userage };
		Result::Ok(new_person)
	}
	else { 
		Result::Err(String::from("Некорректный возраст. Возраст должен быть меньше 110")) 
	}
}

Итак, у нас есть структура Person, которая содержит два поля: name — имя пользователя и age — возраст пользователя. И также определена функция
create_person(), которая принимает данные для имени и возраста пользователя и по ним создает объект Person. Однако данная
функция возвращает не просто объект Person, а объект перечисления Result<Person, String>.
А это значит, что функция должна возвращать либо константу Ok, в которую передается значение Person, либо константу Err, в которую должно передаваться значение String.

Логика функции проста: если возраст корректен (в данном случае меньше 110), то объект Person создается. Если возраст некорретен, то нет смысла создавать
объект Person.

Если действия завершились успешно, то возвращается константа Ok, которая содержит созданный объект Person.

Result::Ok(new_person)

Если действия по созданию объекта Person завершились неудачно, то возвращается константа Err,
которая содержит информацию об ошибке в виде объекта String — некоторое сообщение об ошибке.

Result::Err(String::from("Некорректный возраст. Возраст должен быть меньше 110"))

Теперь применим это в программе:

fn main() {
	// пример корректного ввода - функция возвращает константу Result::Ok
	let tom_result = create_person("Tom", 36);
	match tom_result{
		Ok(tom) => println!("Name: {}  Age: {}", tom.name, tom.age),
		Err(err_message) => println!("{}", err_message)
	}
	
	// пример некорректного ввода - функция возвращает константу Result::Err
	let bob_result = create_person("Bob", 136);
	match bob_result{
		Ok(bob) => println!("Name: {}  Age: {}", bob.name, bob.age),
		Err(err_message) => println!("{}", err_message)
	}
	
    println!("Конец программы...");
}

struct Person{ name: String, age:u8}

fn create_person(username: &str, userage: u8) -> Result<Person, String>{
	
	if userage < 110{
		let new_person = Person{name: String::from(username), age: userage };
		Result::Ok(new_person)
	}
	else { 
		Result::Err(String::from("Некорректный возраст. Возраст должен быть меньше 110")) 
	}
}

Вначале передаем корректные данные:

let tom_result = create_person("Tom", 36);

Чтобы узнать, что собой представляет полученный результат tom_result, используем конструкцию match:

match tom_result{
	Ok(tom) => println!("Name: {}  Age: {}", tom.name, tom.age),
	Err(err_message) => println!("{}", err_message)
}

Если создание объекта Person завершилось успешно, то мы получим константу Ok. В данном случае из константы в переменную tom получаем
возвращенный объект Person и выводим его данные на консоль:

Ok(tom) => println!("Name: {}  Age: {}", tom.name, tom.age),

Если создание Person завершилось неудачно, то выполняется выражение

Err(err_message) => println!("{}", err_message)

которое помещает в переменную err_message объект String с сообщением об ошибке и выводит его на консоль.

Для второго вызова функции логика аналогична. В итоге мы получим следующий консольный вывод:

Name: Tom  Age:36
Некорректный возраст. Возраст должен быть меньше 110
Конец программы...

Вызов макроса panic

Как именно обрабатывать ошибку, зависит от разных условий. Например, в примере выше предполагалось, что отсутствие объекта Person некритично.
Однако вероятны ситуации, когда ошибка может повлиять на возможность дальнейшего выполнения программы. Вполне возможно, что
если объект Person не создан, то дальше нет смысла продолжать работу программы. И в этом случае ее можно завершить макросом panic!:

fn main() {

	let tom_result = create_person("Tom", 36);
	let tom = match tom_result{
		Ok(person) => person,
		Err(err_message) => panic!("Возникла проблема: {}", err_message)
	};
	
	println!("Name: {}  Age: {}", tom.name, tom.age);
}

Здесь, используя синтаксис паттернов, получаем из результата tom_result значение Person. Если результат представляет константу Ok(),
то передаем из нее в переменную person объект Person и возвращаем его:

Ok(person) => person

В итоге это значение перейдет в переменную tom. И далее мы сможем, например, вывести полученные данные на консоль или как-то иным образом использовать
полученный объект Person.

Если результат представляет константу Err, то вызываем макрос panic!, в котором выводит сообщение на консоль. Соответственно
при выполнении этого макроса программа завершает свое выполнение.

Пример выполнения макроса panic при вводе некорректных данных:

enum Result and OK and Err in Rust

Basic Error Handling

Error handling in Rust can be clumsy if you can’t use the question-mark operator.
To achieve happiness, we need to return a Result which can accept any error.
All errors implement the trait std::error::Error, and
so any error can convert into a Box<Error>.

Say we needed to handle both i/o errors and errors from converting
strings into numbers:

# #![allow(unused_variables)]
# 
#fn main() {
// box-error.rs
use std::fs::File;
use std::io::prelude::*;
use std::error::Error;

fn run(file: &str) -> Result<i32,Box<Error>> {
    let mut file = File::open(file)?;
    let mut contents = String::new();
    file.read_to_string(&mut contents)?;
    Ok(contents.trim().parse()?)
}

#}

So that’s two question-marks for the i/o errors (can’t open file, or can’t read as string)
and one question-mark for the conversion error. Finally, we wrap the result in Ok.
Rust can work out from the return type that parse should convert to i32.

It’s easy to create a shortcut for this Result type:

# #![allow(unused_variables)]
# 
#fn main() {
type BoxResult<T> = Result<T,Box<Error>>;

#}

However, our programs will have application-specific error conditions, and so
we need to create our own error type. The basic requirements
are straightforward:

  • May implement Debug
  • Must implement Display
  • Must implement Error

Otherwise, your error can do pretty much what it likes.

# #![allow(unused_variables)]
# 
#fn main() {
// error1.rs
use std::error::Error;
use std::fmt;

#[derive(Debug)]
struct MyError {
    details: String
}

impl MyError {
    fn new(msg: &str) -> MyError {
        MyError{details: msg.to_string()}
    }
}

impl fmt::Display for MyError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f,"{}",self.details)
    }
}

impl Error for MyError {
    fn description(&self) -> &str {
        &self.details
    }
}

// a test function that returns our error result
fn raises_my_error(yes: bool) -> Result<(),MyError> {
    if yes {
        Err(MyError::new("borked"))
    } else {
        Ok(())
    }
}

#}

Typing Result<T,MyError> gets tedious and many Rust modules define their own
Result — e.g. io::Result<T> is short for Result<T,io::Error>.

In this next example we need to handle the specific error when a string can’t be parsed
as a floating-point number.

Now the way that ? works
is to look for a conversion from the error of the expression to the error that must
be returned. And this conversion is expressed by the From trait. Box<Error>
works as it does because it implements From for all types implementing Error.

At this point you can continue to use the convenient alias BoxResult and catch everything
as before; there will be a conversion from our error into Box<Error>.
This is a good option for smaller applications. But I want to show other errors can
be explicitly made to cooperate with our error type.

ParseFloatError implements Error so description() is defined.

# #![allow(unused_variables)]
# 
#fn main() {
use std::num::ParseFloatError;

impl From<ParseFloatError> for MyError {
    fn from(err: ParseFloatError) -> Self {
        MyError::new(err.description())
    }
}

// and test!
fn parse_f64(s: &str, yes: bool) -> Result<f64,MyError> {
    raises_my_error(yes)?;
    let x: f64 = s.parse()?;
    Ok(x)
}

#}

The first ? is fine (a type always converts to itself with From) and the
second ? will convert the ParseFloatError to MyError.

And the results:

fn main() {
    println!(" {:?}", parse_f64("42",false));
    println!(" {:?}", parse_f64("42",true));
    println!(" {:?}", parse_f64("?42",false));
}
//  Ok(42)
//  Err(MyError { details: "borked" })
//  Err(MyError { details: "invalid float literal" })

Not too complicated, although a little long-winded. The tedious bit is having to
write From conversions for all the other error types that need to play nice
with MyError — or you simply lean on Box<Error>. Newcomers get confused
by the multitude of ways to do the same thing in Rust; there is always another
way to peel the avocado (or skin the cat, if you’re feeling bloodthirsty). The price
of flexibility is having many options. Error-handling for a 200 line program can afford
to be simpler than for a large application. And if you ever want to package your precious
droppings as a Cargo crate, then error handling becomes crucial.

Currently, the question-mark operator only works for Result, not Option, and this is
a feature, not a limitation. Option has a ok_or_else which converts itself into a Result.
For example, say we had a HashMap and must fail if a key isn’t defined:

# #![allow(unused_variables)]
# 
#fn main() {
    let val = map.get("my_key").ok_or_else(|| MyError::new("my_key not defined"))?;

#}

Now here the error returned is completely clear! (This form uses a closure, so the error value
is only created if the lookup fails.)

simple-error for Simple Errors

The simple-error crate provides you with
a basic error type based on a string, as we have defined it here, and a few convenient macros.
Like any error, it works fine with Box<Error>:

#[macro_use]
extern crate simple_error;

use std::error::Error;

type BoxResult<T> = Result<T,Box<Error>>;

fn run(s: &str) -> BoxResult<i32> {
    if s.len() == 0 {
        bail!("empty string");
    }
    Ok(s.trim().parse()?)
}

fn main() {
    println!("{:?}", run("23"));
    println!("{:?}", run("2x"));
    println!("{:?}", run(""));
}
// Ok(23)
// Err(ParseIntError { kind: InvalidDigit })
// Err(StringError("empty string"))

bail!(s) expands to return SimpleError::new(s).into(); — return early with a conversion into
the receiving type.

You need to use BoxResult for mixing the SimpleError type with other errors, since
we can’t implement From for it, since both the trait and the type come from other crates.

error-chain for Serious Errors

For non-trivial applications have a look
at the error_chain crate.
A little macro magic can go a long way in Rust…

Create a binary crate with cargo new --bin test-error-chain and
change to this directory. Edit Cargo.toml and add error-chain="0.8.1" to the end.

What error-chain does for you is create all the definitions we needed for manually implementing
an error type; creating a struct, and implementing the necessary traits: Display, Debug and Error.
It also by default implements From so strings can be converted into errors.

Our first src/main.rs file looks like this. All the main program does is call run, print out any
errors, and end the program with a non-zero exit code. The macro error_chain generates all the
definitions needed, within an error module — in a larger program you would put this in its own file.
We need to bring everything in error back into global scope because our code will need to see
the generated traits. By default, there will be an Error struct and a Result defined with that
error.

Here we also ask for From to be implemented so that std::io::Error will convert into
our error type using foreign_links:

#[macro_use]
extern crate error_chain;

mod errors {
    error_chain!{
        foreign_links {
            Io(::std::io::Error);
        }
    }
}
use errors::*;

fn run() -> Result<()> {
    use std::fs::File;

    File::open("file")?;

    Ok(())
}


fn main() {
    if let Err(e) = run() {
        println!("error: {}", e);

        std::process::exit(1);
    }
}
// error: No such file or directory (os error 2)

The ‘foreign_links’ has made our life easier, since the question mark operator now knows how to
convert std::io::Error into our error::Error. (Under the hood, the macro is creating a
From<std::io::Error> conversion, exactly as spelt out earlier.)

All the action happens in run; let’s make it print out the first 10 lines of a file given as the
first program argument. There may or may not be such an argument, which isn’t necessarily an
error. Here we want to convert an Option<String> into a Result<String>. There are two Option
methods for doing this conversion, and I’ve picked the simplest one. Our Error type implements
From for &str, so it’s straightforward to make an error with a simple text message.

# #![allow(unused_variables)]
# 
#fn main() {
fn run() -> Result<()> {
    use std::env::args;
    use std::fs::File;
    use std::io::BufReader;
    use std::io::prelude::*;

    let file = args().skip(1).next()
        .ok_or(Error::from("provide a file"))?;

    let f = File::open(&file)?;
    let mut l = 0;
    for line in BufReader::new(f).lines() {
        let line = line?;
        println!("{}", line);
        l += 1;
        if l == 10 {
            break;
        }
    }

    Ok(())
}

#}

There is (again) a useful little macro bail! for ‘throwing’ errors.
An alternative to the ok_or method here could be:

# #![allow(unused_variables)]
# 
#fn main() {
    let file = match args().skip(1).next() {
        Some(s) => s,
        None => bail!("provide a file")
    };

#}

Like ? it does an early return.

The returned error contains an enum ErrorKind, which allows us to distinguish between various
kinds of errors. There’s always a variant Msg (when you say Error::from(str)) and the foreign_links
has declared Io which wraps I/O errors:

fn main() {
    if let Err(e) = run() {
        match e.kind() {
            &ErrorKind::Msg(ref s) => println!("msg {}",s),
            &ErrorKind::Io(ref s) => println!("io {}",s),
        }
        std::process::exit(1);
    }
}
// $ cargo run
// msg provide a file
// $ cargo run foo
// io No such file or directory (os error 2)

It’s straightforward to add new kinds of errors. Add an errors section to the error_chain! macro:

# #![allow(unused_variables)]
# 
#fn main() {
    error_chain!{
        foreign_links {
            Io(::std::io::Error);
        }

        errors {
            NoArgument(t: String) {
                display("no argument provided: '{}'", t)
            }
        }

    }

#}

This defines how Display works for this new kind of error. And now we can handle
‘no argument’ errors more specifically, feeding ErrorKind::NoArgument a String value:

# #![allow(unused_variables)]
# 
#fn main() {
    let file = args().skip(1).next()
        .ok_or(ErrorKind::NoArgument("filename needed".to_string()))?;


#}

There’s now an extra ErrorKind variant that you must match:

fn main() {
    if let Err(e) = run() {
        println!("error {}",e);
        match e.kind() {
            &ErrorKind::Msg(ref s) => println!("msg {}", s),
            &ErrorKind::Io(ref s) => println!("io {}", s),
            &ErrorKind::NoArgument(ref s) => println!("no argument {:?}", s),
        }
        std::process::exit(1);
    }
}
// cargo run
// error no argument provided: 'filename needed'
// no argument "filename needed"

Generally, it’s useful to make errors as specific as possible, particularly if this is a library
function! This match-on-kind technique is pretty much the equivalent of traditional exception handling,
where you match on exception types in a catch or except block.

In summary, error-chain creates a type Error for you, and defines Result<T> to be std::result::Result<T,Error>.
Error contains an enum ErrorKind and by default there is one variant Msg for errors created from
strings. You define external errors with foreign_links which does two things. First, it creates a new
ErrorKind variant. Second, it defines From on these external errors so they can be converted to our
error. New error variants can be easily added. A lot of irritating boilerplate code is eliminated.

Chaining Errors

But the really cool thing that this crate provides is error chaining.

As a library user, it’s irritating when a method simply just ‘throws’ a generic I/O error. OK, it
could not open a file, fine, but what file? Basically, what use is this information to me?

error_chain does error chaining which helps solve this problem of over-generic errors. When we
try to open the file, we can lazily lean on the conversion to io::Error using ?, or chain the error.

# #![allow(unused_variables)]
# 
#fn main() {
// non-specific error
let f = File::open(&file)?;

// a specific chained error
let f = File::open(&file).chain_err(|| "unable to read the damn file")?;

#}

Here’s a new version of the program, with no imported ‘foreign’ errors, just the defaults:

#[macro_use]
extern crate error_chain;

mod errors {
    error_chain!{
    }

}
use errors::*;

fn run() -> Result<()> {
    use std::env::args;
    use std::fs::File;
    use std::io::BufReader;
    use std::io::prelude::*;

    let file = args().skip(1).next()
        .ok_or(Error::from("filename needed"))?;

    ///////// chain explicitly! ///////////
    let f = File::open(&file).chain_err(|| "unable to read the damn file")?;

    let mut l = 0;
    for line in BufReader::new(f).lines() {
        let line = line.chain_err(|| "cannot read a line")?;
        println!("{}", line);
        l += 1;
        if l == 10 {
            break;
        }
    }

    Ok(())
}


fn main() {
    if let Err(e) = run() {
        println!("error {}", e);

        /////// look at the chain of errors... ///////
        for e in e.iter().skip(1) {
            println!("caused by: {}", e);
        }

        std::process::exit(1);
    }
}
// $ cargo run foo
// error unable to read the damn file
// caused by: No such file or directory (os error 2)

So the chain_err method takes the original error, and creates a new error which contains the
original error — this can be continued indefinitely. The closure is expected to return any
value which can be converted into an error.

Rust macros can clearly save you a lot of typing. error-chain even provides a shortcut that
replaces the whole main program:

# #![allow(unused_variables)]
# 
#fn main() {
quick_main!(run);

#}

(run is where all the action takes place, anyway.)

Error handling in Rust is very different if you’re coming from other languages. In languages like Java, JS, Python etc, you usually throw exceptions and return successful values. In Rust, you return something called a Result.

The Result<T, E> type is an enum that has two variants — Ok(T) for successful value or Err(E) for error value:

enum Result<T, E> {
   Ok(T),
   Err(E),
}

Returning errors instead of throwing them is a paradigm shift in error handling. If you’re new to Rust, there will be some friction initially as it requires you to reason about errors in a different way.

In this post, I’ll go through some common error handling patterns so you gradually become familiar with how things are done in Rust:

  • Ignore the error
  • Terminate the program
  • Use a fallback value
  • Bubble up the error
  • Bubble up multiple errors
  • Match boxed errors
  • Libraries vs Applications
  • Create custom errors
  • Bubble up custom errors
  • Match custom errors

Ignore the error

Let’s start with the simplest scenario where we just ignore the error. This sounds careless but has a couple of legitimate use cases:

  • We’re prototyping our code and don’t want to spend time on error handling.
  • We’re confident that the error won’t occur.

Let’s say that we’re reading a file which we’re pretty sure would be present:

use std::fs;

fn main() {
  let content = fs::read_to_string("./Cargo.toml").unwrap();
  println!("{}", content)
}

Even though we know that the file would be present, the compiler has no way of knowing that. So we use unwrap to tell the compiler to trust us and return the value inside. If the read_to_string function returns an Ok() value, unwrap will get the contents of Ok and assign it to the content variable. If it returns an error, it will “panic”. Panic either terminates the program or exits the current thread.

Note that unwrap is used in quite a lot of Rust examples to skip error handling. This is mostly done for convenience and shouldn’t be used in real code as it is.

Terminate the program

Some errors cannot be handled or recovered from. In these cases, it’s better to fail fast by terminating the program.

Let’s use the same example as above — we’re reading a file which we’re sure to be present. Let’s imagine that, for this program, that file is absolutely important without which it won’t work properly. If for some reason, this file is absent, it’s better to terminate the program.

We can use unwrap as before or use expect — it’s same as unwrap but lets us add extra error message.

use std::fs;

fn main() {
  let content = fs::read_to_string("./Cargo.toml").expect("Can't read Cargo.toml");
  println!("{}", content)
}

See also: panic!

Use a fallback value

In some cases, you can handle the error by falling back to a default value.

For example, let’s say we’re writing a server and the port it listens to can be configured using an environment variable. If the environment variable is not set, accessing that value would result in an error. But we can easily handle that by falling back to a default value.

use std::env;

fn main() {
  let port = env::var("PORT").unwrap_or("3000".to_string());
  println!("{}", port);
}

Here, we’ve used a variation of unwrap called unwrap_or which lets us supply default values.

See also: unwrap_or_else, unwrap_or_default

Bubble up the error

When you don’t have enough context to handle the error, you can bubble up (propagate) the error to the caller function.

Here’s a contrived example which uses a webservice to get the current year:

use std::collections::HashMap;

fn main() {
  match get_current_date() {
    Ok(date) => println!("We've time travelled to {}!!", date),
    Err(e) => eprintln!("Oh noes, we don't know which era we're in! :( n  {}", e),
  }
}

fn get_current_date() -> Result<String, reqwest::Error> {
  let url = "https://postman-echo.com/time/object";
  let result = reqwest::blocking::get(url);

  let response = match result {
    Ok(res) => res,
    Err(err) => return Err(err),
  };

  let body = response.json::<HashMap<String, i32>>();

  let json = match body {
    Ok(json) => json,
    Err(err) => return Err(err),
  };

  let date = json["years"].to_string();

  Ok(date)
}

There are two function calls inside the get_current_date function (get and json) that return Result values. Since get_current_date doesn’t have context of what to do when they return errors, it uses pattern matching to propagate the errors to main.

Using pattern matching to handle multiple or nested errors can make your code “noisy”. Instead, we can rewrite the above code using the ? operator:

use std::collections::HashMap;

fn main() {
  match get_current_date() {
    Ok(date) => println!("We've time travelled to {}!!", date),
    Err(e) => eprintln!("Oh noes, we don't know which era we're in! :( n  {}", e),
  }
}

fn get_current_date() -> Result<String, reqwest::Error> {
  let url = "https://postman-echo.com/time/object";
  let res = reqwest::blocking::get(url)?.json::<HashMap<String, i32>>()?;
  let date = res["years"].to_string();

  Ok(date)
}

This looks much cleaner!

The ? operator is similar to unwrap but instead of panicking, it propagates the error to the calling function. One thing to keep in mind is that we can use the ? operator only for functions that return a Result or Option type.

Bubble up multiple errors

In the previous example, the get and json functions return a reqwest::Error error which we’ve propagated using the ? operator. But what if we’ve another function call that returned a different error value?

Let’s extend the previous example by returning a formatted date instead of the year:

+ use chrono::NaiveDate;
  use std::collections::HashMap;

  fn main() {
    match get_current_date() {
      Ok(date) => println!("We've time travelled to {}!!", date),
      Err(e) => eprintln!("Oh noes, we don't know which era we're in! :( n  {}", e),
    }
  }

  fn get_current_date() -> Result<String, reqwest::Error> {
    let url = "https://postman-echo.com/time/object";
    let res = reqwest::blocking::get(url)?.json::<HashMap<String, i32>>()?;
-   let date = res["years"].to_string();
+   let formatted_date = format!("{}-{}-{}", res["years"], res["months"] + 1, res["date"]);
+   let parsed_date = NaiveDate::parse_from_str(formatted_date.as_str(), "%Y-%m-%d")?;
+   let date = parsed_date.format("%Y %B %d").to_string();

    Ok(date)
  }

The above code won’t compile as parse_from_str returns a chrono::format::ParseError error and not reqwest::Error.

We can fix this by Boxing the errors:

  use chrono::NaiveDate;
  use std::collections::HashMap;

  fn main() {
    match get_current_date() {
      Ok(date) => println!("We've time travelled to {}!!", date),
      Err(e) => eprintln!("Oh noes, we don't know which era we're in! :( n  {}", e),
    }
  }

- fn get_current_date() -> Result<String, reqwest::Error> {
+ fn get_current_date() -> Result<String, Box<dyn std::error::Error>> {
    let url = "https://postman-echo.com/time/object";
    let res = reqwest::blocking::get(url)?.json::<HashMap<String, i32>>()?;

    let formatted_date = format!("{}-{}-{}", res["years"], res["months"] + 1, res["date"]);
    let parsed_date = NaiveDate::parse_from_str(formatted_date.as_str(), "%Y-%m-%d")?;
    let date = parsed_date.format("%Y %B %d").to_string();

    Ok(date)
  }

Returning a trait object Box<dyn std::error::Error> is very convenient when we want to return multiple errors!

See also: anyhow, eyre

Match boxed errors

So far, we’ve only printed the errors in the main function but not handled them. If we want to handle and recover from boxed errors, we need to “downcast” them:

  use chrono::NaiveDate;
  use std::collections::HashMap;

  fn main() {
    match get_current_date() {
      Ok(date) => println!("We've time travelled to {}!!", date),
-     Err(e) => eprintln!("Oh noes, we don't know which era we're in! :( n  {}", e),
+     Err(e) => {
+       eprintln!("Oh noes, we don't know which era we're in! :(");
+       if let Some(err) = e.downcast_ref::<reqwest::Error>() {
+         eprintln!("Request Error: {}", err)
+       } else if let Some(err) = e.downcast_ref::<chrono::format::ParseError>() {
+         eprintln!("Parse Error: {}", err)
+       }
+     }
    }
  }

  fn get_current_date() -> Result<String, Box<dyn std::error::Error>> {
    let url = "https://postman-echo.com/time/object";
    let res = reqwest::blocking::get(url)?.json::<HashMap<String, i32>>()?;

    let formatted_date = format!("{}-{}-{}", res["years"], res["months"] + 1, res["date"]);
    let parsed_date = NaiveDate::parse_from_str(formatted_date.as_str(), "%Y-%m-%d")?;
    let date = parsed_date.format("%Y %B %d").to_string();

    Ok(date)
  }

Notice how we need to be aware of the implementation details (different errors inside) of get_current_date to be able to downcast them inside main.

See also: downcast, downcast_mut

Applications vs Libraries

As mentioned previously, the downside to boxed errors is that if we want to handle the underlying errors, we need to be aware of the implementation details. When we return something as Box<dyn std::error::Error>, the concrete type information is erased. To handle the different errors in different ways, we need to downcast them to concrete types and this casting can fail at runtime.

However, saying something is a “downside” is not very useful without context. A good rule of thumb is to question whether the code you’re writing is an “application” or a “library”:

Application

  • The code you’re writing would be used by end users.
  • Most errors generated by application code won’t be handled but instead logged or reported to the user.
  • It’s okay to use boxed errors.

Library

  • The code you’re writing would be consumed by other code. A “library” can be open source crates, internal libraries etc
  • Errors are part of your library’s API, so your consumers know what errors to expect and recover from.
  • Errors from your library are often handled by your consumers so they need to be structured and easy to perform exhaustive match on.
  • If you return boxed errors, then your consumers need to be aware of the errors created by your code, your dependencies, and so on!
  • Instead of boxed errors, we can return custom errors.

Create custom errors

For library code, we can convert all the errors to our own custom error and propagate them instead of boxed errors. In our example, we currently have two errors — reqwest::Error and chrono::format::ParseError. We can convert them to MyCustomError::HttpError and MyCustomError::ParseError respectively.

Let’s start by creating an enum to hold our two error variants:

// error.rs

pub enum MyCustomError {
  HttpError,
  ParseError,
}

The Error trait requires us to implement the Debug and Display traits:

// error.rs

use std::fmt;

#[derive(Debug)]
pub enum MyCustomError {
  HttpError,
  ParseError,
}

impl std::error::Error for MyCustomError {}

impl fmt::Display for MyCustomError {
  fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
    match self {
      MyCustomError::HttpError => write!(f, "HTTP Error"),
      MyCustomError::ParseError => write!(f, "Parse Error"),
    }
  }
}

We’ve created our own custom error!

This is obviously a simple example as the error variants don’t contain much information about the error. But this should be sufficient as a starting point for creating more complex and realistic custom errors. Here are some real life examples: ripgrep, reqwest, csv and serde_json

See also: thiserror, snafu

Bubble up custom errors

Let’s update our code to return the custom errors we just created:

  // main.rs

+ mod error;

  use chrono::NaiveDate;
+ use error::MyCustomError;
  use std::collections::HashMap;

  fn main() {
    // skipped, will get back later
  }

- fn get_current_date() -> Result<String, Box<dyn std::error::Error>> {
+ fn get_current_date() -> Result<String, MyCustomError> {
    let url = "https://postman-echo.com/time/object";
-   let res = reqwest::blocking::get(url)?.json::<HashMap<String, i32>>()?;
+   let res = reqwest::blocking::get(url)
+     .map_err(|_| MyCustomError::HttpError)?
+     .json::<HashMap<String, i32>>()
+     .map_err(|_| MyCustomError::HttpError)?;

    let formatted_date = format!("{}-{}-{}", res["years"], res["months"] + 1, res["date"]);
-   let parsed_date = NaiveDate::parse_from_str(formatted_date.as_str(), "%Y-%m-%d")?;
+   let parsed_date = NaiveDate::parse_from_str(formatted_date.as_str(), "%Y-%m-%d")
+     .map_err(|_| MyCustomError::ParseError)?;
    let date = parsed_date.format("%Y %B %d").to_string();

    Ok(date)
  }

Notice how we’re using map_err to convert the error from one type to another type.

But things got verbose as a result — our function is littered with these map_err calls. We can implement the From trait to automatically coerce the error types when we use the ? operator:

  // error.rs

  use std::fmt;

  #[derive(Debug)]
  pub enum MyCustomError {
    HttpError,
    ParseError,
  }

  impl std::error::Error for MyCustomError {}

  impl fmt::Display for MyCustomError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
      match self {
        MyCustomError::HttpError => write!(f, "HTTP Error"),
        MyCustomError::ParseError => write!(f, "Parse Error"),
      }
    }
  }

+ impl From<reqwest::Error> for MyCustomError {
+   fn from(_: reqwest::Error) -> Self {
+     MyCustomError::HttpError
+   }
+ }

+ impl From<chrono::format::ParseError> for MyCustomError {
+   fn from(_: chrono::format::ParseError) -> Self {
+     MyCustomError::ParseError
+   }
+ }
  // main.rs

  mod error;

  use chrono::NaiveDate;
  use error::MyCustomError;
  use std::collections::HashMap;

  fn main() {
    // skipped, will get back later
  }

  fn get_current_date() -> Result<String, MyCustomError> {
    let url = "https://postman-echo.com/time/object";
-   let res = reqwest::blocking::get(url)
-     .map_err(|_| MyCustomError::HttpError)?
-     .json::<HashMap<String, i32>>()
-     .map_err(|_| MyCustomError::HttpError)?;
+   let res = reqwest::blocking::get(url)?.json::<HashMap<String, i32>>()?;

    let formatted_date = format!("{}-{}-{}", res["years"], res["months"] + 1, res["date"]);
-   let parsed_date = NaiveDate::parse_from_str(formatted_date.as_str(), "%Y-%m-%d")
-     .map_err(|_| MyCustomError::ParseError)?;
+   let parsed_date = NaiveDate::parse_from_str(formatted_date.as_str(), "%Y-%m-%d")?;
    let date = parsed_date.format("%Y %B %d").to_string();

    Ok(date)
  }

We’ve removed map_err and the code looks much cleaner!

However, From trait is not magic and there are times when we need to use map_err. In the above example, we’ve moved the type conversion from inside the get_current_date function to the From<X> for MyCustomError implementation. This works well when the information needed to convert from one error to MyCustomError can be obtained from the original error object. If not, we need to use map_err inside get_current_date.

Match custom errors

We’ve ignored the changes in main until now, here’s how we can handle the custom errors:

  // main.rs

  mod error;

  use chrono::NaiveDate;
  use error::MyCustomError;
  use std::collections::HashMap;

  fn main() {
    match get_current_date() {
      Ok(date) => println!("We've time travelled to {}!!", date),
      Err(e) => {
        eprintln!("Oh noes, we don't know which era we're in! :(");
-       if let Some(err) = e.downcast_ref::<reqwest::Error>() {
-         eprintln!("Request Error: {}", err)
-       } else if let Some(err) = e.downcast_ref::<chrono::format::ParseError>() {
-         eprintln!("Parse Error: {}", err)
-       }
+       match e {
+         MyCustomError::HttpError => eprintln!("Request Error: {}", e),
+         MyCustomError::ParseError => eprintln!("Parse Error: {}", e),
+       }
      }
    }
  }

  fn get_current_date() -> Result<String, MyCustomError> {
    let url = "https://postman-echo.com/time/object";
    let res = reqwest::blocking::get(url)?.json::<HashMap<String, i32>>()?;

    let formatted_date = format!("{}-{}-{}", res["years"], res["months"] + 1, res["date"]);
    let parsed_date = NaiveDate::parse_from_str(formatted_date.as_str(), "%Y-%m-%d")?;
    let date = parsed_date.format("%Y %B %d").to_string();

    Ok(date)
  }

Notice how unlike boxed errors, we can actually match on the variants inside MyCustomError enum.

Conclusion

Thanks for reading! I hope this post was helpful in introducing the basics of error handling in Rust. I’ve added the examples to a repo in GitHub which you can use for practice. If you’ve more questions, please contact me at sheshbabu [at] gmail.com. Feel free to follow me in Twitter for more posts like this :)

Options and Results

The Option and Result types in Rust will be two of the most used types you will have at your disposal when writing your programs. Their concepts are simple but their use can be confusing at times for beginners. It was for me. This blog entry is an attempt to help explain how to use them effectively.

A Result can also wrap a Rust error and this blog article will cover how to create those easily too.

Let’s look at the basics.

Option

The Option type allows you to have a variable that may or may not contain a value. This is useful for passing optional parameters or as a return value from a function that may or may not succeed.

Its definition is

enum Option<T> {
    Some(T),
    None,
}

Enter fullscreen mode

Exit fullscreen mode

So it can either contain a single value via the Some variant or no value at all via the None variant.

Enums in Rust are implemented with a discriminant value that tells Rust what type of variant is stored and a union of all the data in the variants. So, if you were to implement the same thing in C, it would look something like:

struct Option
{
    int type;
    union 
    {
        struct Some 
        {
            T t;
        };
        struct None {};
    };
};

Enter fullscreen mode

Exit fullscreen mode

So, the size of the enum is usually the size of the largest variant plus the size of the type value.

But Rust has a neat optimisation. If one variant has no data and the other has a single value that is a non-null pointer (such as references, boxes, function pointers), Rust will optimise the enum type so that its size is the same as the type T. It accomplishes this by representing the no-value variant (e.g. None) as a null pointer. This means something like Option<&T> is the same size as &T. Effectively, this is like normal C pointers with the extra type safety.

For more information about this check out the documentation of Option here under the section titled ‘Representation’.

Below is an example of how we can use Option for a generic function that returns the first item:

fn first_item<T>(v: &Vec<T>) -> Option<T>
where T: Clone {
    if v.len() > 0 {
        Some(v[0].clone())
    } else {
        None
    }
}

Enter fullscreen mode

Exit fullscreen mode

The function first_item can only return a value if the vector being passed is not empty. This is a good candidate for Option. If the vector is empty, we return None, otherwise we return a copy of the value via Some.

The None variant forces the programmer to consider the case where the information required is not forthcoming.

Result

Result is similar to Option in that it can either return a value or it doesn’t and is usually used as a return value from a function. But instead of returning a None value, it returns an error value that hopefully encapsulates the information of why it went wrong.

Its form is:

enum Result<T, E> {
    Ok(T),
    Err(E),
}

Enter fullscreen mode

Exit fullscreen mode

If everything goes well, a function can return an Ok variant along with the final result of the function. However, if something fails within the function it can return an Err variant along with the error value.

Let’s look at an example:

use std::fs::File;
use std::io::{BufRead, BufReader, Error};

fn first_line(path: &str) -> Result<String, Error> {
    let f = File::open(path);

    match f {
        Ok(f) => {
            let mut buf = BufReader::new(f);
            let mut line = String::new();
            match buf.read_line(&mut line) {
                Ok(_) => Ok(line),
                Err(e) => Err(e),
            }
        }
        Err(e) => Err(e),
    }
}

Enter fullscreen mode

Exit fullscreen mode

std::fs::File::open will return a Result<std::fs::File, std::io::Error>. That is, it will either return a file handle if everything goes OK, or it will return an I/O error if it doesn’t. We can match on this. If it’s an error, we just return it immediately. Otherwise, we try to read the first line of that file via the std::io::BufReader type.

The read_line method returns a Result<String, std::io:Error> and once again we match on this. If it was an error, we return it immediately. Notice that the error type for both the open and read_line methods is std::io::Error. If they were different, this function wouldn’t compile. We will deal with differing error types later.

However, if we were successful, we return the first line as a string via the Ok variant.

The ? operator

Rust introduced an operator ? that made handling errors less verbose. Basically, it turns code like this:

let x = function_that_may_fail();
let value = match x {
    Ok(v) => value,
    Err(e) => return Err(e);
}

Enter fullscreen mode

Exit fullscreen mode

into:

let value = function_that_may_fail()?;

Enter fullscreen mode

Exit fullscreen mode

The ? operator changes the Result<T,E> value into one of type T. However, if the result was an error, the current function exits immediately with the same ‘Err’ variant. It unwraps the result if everything went OK, or it causes the function to return with an error if not.

With this in mind, we can simplify the first_line demo function above:

use std::fs::File;
use std::io::{BufRead, BufReader, Error};

fn first_line(path: &str) -> Result<String, Error> {
    let f = File::open(path)?;
    let mut buf = BufReader::new(f);
    let mut line = String::new();
    buf.read_line(&mut line)?;
    Ok(line)
}

Enter fullscreen mode

Exit fullscreen mode

I think we can all agree this is a lot easier to read.

Errors

The error type in a Result can be any type, like, for example, a String. However, it is recommended to use a type that implements the trait std::error::Error. By using this standard trait, users can handle your errors better and even aggregate them.

Traits are interfaces that structures can implement as methods to extend them. I might write a blog article about traits in the future, but if you are not sure what they are, please read this article.

Here is the Error trait in all its glory:

trait Error: Debug + Display {
    fn source(&self) -> Option<&(dyn Error + 'static)> { None };
    fn backtrace(&self) -> Option<&Backtrace> { None };
}

Enter fullscreen mode

Exit fullscreen mode

The backtrace method is only defined in the nightly version of the compiler, at time of writing, and so only source is defined for the stable version. source can be implemented to return an earlier error that this current error would be chained to. But if there is no previous error, None is returned. Returning None is the default implementation of this method.

A type that implements Error must also implement Debug and Display traits.

Errors can be enums too. Below is an example of possible errors that can occur when reading from a file-based database:

use std::fmt::{Result, Formatter};
use std::fs::File;

#[derive(Debug)]
enum MyError {
    DatabaseNotFound(String),
    CannotOpenDatabase(String),
    CannotReadDatabase(String, File),
}

impl std::error::Error for MyError{}

impl std::fmt::Display for MyError {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
        match self {
            Self::DatabaseNotFound(ref str) => write!(f, "File `{}` not found", str),
            Self::CannotOpenDatabase(ref String) => write!(f, "Cannot open database: {}", str),
            Self::CannotReadDatabase(ref String, _) => write!(f, "Cannot read database: {}", str),
        }
    }
}

Enter fullscreen mode

Exit fullscreen mode

First we define the enum with the possible error states and their associative data (e.g. filename of file that was not found). Notice the derive macro that implements the Debug trait for us. Unfortunately, we cannot do that for Display traits.

Secondly, we implement the Error trait for compatibility with other error systems. Since we’re not chaining errors, the default implementation will do.

Finally, we implement the Display trait, which is a requirement of the Errortrait.

This is a lot to write for an error type, but fortunately there are some popular crates that allow us to write and use errors more easily.

Error Crates

As just shown, implementing an error type to be passed with a Result‘s Err variant can be tedious to write. Some consider the Error trait lacking in functionality too. Various crates have been written to combat the boilerplate and to increase the usefulness of the types of error values you can generate.

I will explore a few of them in this article that I have found to be the most popular.

Let us imagine we want to implement this function:

fn first_line(path: &str) -> Result<String, FirstLineError> { ... }

Enter fullscreen mode

Exit fullscreen mode

We will investigate how we implement FirstLineError in each of these crates. The basic foundation of the error will be this enum:

enum FirstLineError {
    CannotOpenFile { name: String },
    NoLines,
}

Enter fullscreen mode

Exit fullscreen mode

failure crate

failure provides 2 major concepts: the Fail trait and an Error type.

The Fail trait is a new custom error type specifically to hold better error information. This trait is used by libraries to define new error types.

The Error trait is a wrapper around the Fail types that can be used to compose higher-level errors. For example, a file open error can be linked to a database open error. The user would deal with the database open error, and could dig down further and obtain the original file error if they wanted.

Generally, crate writers would use Fail and crate users would interact with the Error types.

Failure also supports backtraces if the crate feature backtrace is enabled and the RUST_BACKTRACE environment variable is set to 1.

This is how we would create the FirstLineError error type using this crate:

use std::fs::File;
use std::io::{BufRead, BufReader};

use failure::Fail;

#[derive(Fail, Debug)]
enum FirstLineError {
    #[fail(display = "Cannot open file `{}`", name)]
    CannotOpenFile { name: String },
    #[fail(display = "No lines found")]
    NoLines,
}

fn first_line(path: &str) -> Result<String, FirstLineError> {
    let f = File::open(path).map_err(|_| FirstLineError::CannotOpenFile {
        name: String::from(path),
    })?;
    let mut buf = BufReader::new(f);
    let mut line = String::new();
    buf.read_line(&mut line)
        .map_err(|_| FirstLineError::NoLines)?;
    Ok(line)
}

Enter fullscreen mode

Exit fullscreen mode

The derive macro implements the Fail and Display traits automatically for us. It uses the fail attributes to help it construct those traits.

But we do have another problem. The File::open and BufRead::read_line methods return a result based on the std::io::Error type and not the FirstLineError type that we require. We use the Result‘s map_err method to convert one error type to another.

I will cover map_err and other methods for Option and Result in my next blog article, but for now I will describe this one. If the result is an error, map_err will call the closure given with the error value allowing us an opportunity to replace it with a different error value.

So, recall that File::open returns a Result<(), std::io::Error value. By calling map_err we now return a Result<(), FirstLineError> value. This is because the closure given returns a FirstLineError value and through type inference, we get the new result type. If the result is an error, that closure will provide the value to associate with the Err variant.

But the value returned from File::open is still a Result type so we use the ? operator to exit immediately if an error occurs.

Now we can do things like:

match first_line("foo.txt") {
    Ok(line) => println!("First line: {}", line),
    Err(e) => println!("Error occurred: {}", e),
}

Enter fullscreen mode

Exit fullscreen mode

Failure can even allow you to create errors on the fly that are compatible with failure::Error. For example,

use failure::{ensure, Error};

fn check_even(num: i32) -> Result<(), Error> {
    ensure!(num % 2 == 0, "Number is not even");
    Ok(())
}

fn main() {
    match check_even(41) {
        Ok(()) => println!("It's even!"),
        Err(e) => println!("{}", e),
    }
}

Enter fullscreen mode

Exit fullscreen mode

This program will output Number is not even as expected via the Display trait of the error.

There are other ways to create errors on the fly too with failure. format_err! will create a string based error:

let err = format_err!("File not found: {}", file_name);

Enter fullscreen mode

Exit fullscreen mode

And finally, there’s a macro that combines format_err! with a return:

bail!("File not found: {}", file_name);

Enter fullscreen mode

Exit fullscreen mode

snafu crate

This is similar to failure but solves the issue where the actual error that occurred is not the error we want to report.

If you recall above, we use map_err to convert the std::io::Error into one of our FirstLineError variants. snafu makes this easier by providing a context method that allows the programmer to pass in the actual error they wish to report.

Let’s redefine our error type and function using snafu:

use std::fs::File;
use std::io::{BufRead, BufReader};

use snafu::{Snafu, ResultExt};

#[derive(Snafu, Debug)]
enum FirstLineError {
    #[snafu(display("Cannot open file {} because: {}", name, source))]
    CannotOpenFile {
        name: String,
        source: std::io::Error,
    },
    #[snafu(display("No lines found because: {}", source))]
    NoLines { source: std::io::Error },
}

fn first_line(path: &str) -> Result<String, FirstLineError> {
    let f = File::open(path).context(CannotOpenFile {
        name: String::from(path),
    })?;
    let mut buf = BufReader::new(f);
    let mut line = String::new();
    buf.read_line(&mut line)
        .context(NoLines)?;
    Ok(line)
}

Enter fullscreen mode

Exit fullscreen mode

To use context(), there needs to be a source field in the variant. Notice that the enum type FirstLineError is not included. We wrote CannotOpenFile, not FirstLineError::CannotOpenFile. And the source field is automatically set! There’s some black magic going on there!

If you don’t want to use the name source for your underlying cause, you can rename it by marking the field you do want to be the source with #[snafu(source)]. Also, if there is a field called source that you don’t want to be treated as snafu‘s source field, mark it with #[snafu(source(false))].

Similarly, snafu supports the backtrace field too to store a backtrace at point of error. #[snafu(backtrace)] et al. controls those fields like the source.

On top of this, you have the ensure! macro that functions like failure‘s.

anyhow crate

This crate provides dynamic error support via its anyhow::Result<T>. This type can receive any error. It can create an ad-hoc error from a string using anyhow!:

let err = anyhow!("File not found: {}", file_name);

Enter fullscreen mode

Exit fullscreen mode

It also defines bail! and ensure! like other crates. anyhow results can extend the errors using a context() method:

let err = anyhow!("File not found: {}", file_name)
    .context("Tried to load the configuration file");

Enter fullscreen mode

Exit fullscreen mode

Here’s the first_line method implemented using anyhow:

use anyhow::Result;

#[derive(Debug)]
enum FirstLineError {
    CannotOpenFile {
        name: String,
        source: std::io::Error,
    },
    NoLines {
        source: std::io::Error,
    },
}

impl std::error::Error for FirstLineError {}

impl std::fmt::Display for FirstLineError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            FirstLineError::CannotOpenFile { name, source } => {
                write!(f, "Cannot open file `{}` because: {}", name, source)
            }
            FirstLineError::NoLines { source } => {
                write!(f, "Cannot find line in file because: {}", source)
            }
        }
    }
}

fn first_line(path: &str) -> Result<String> {
    let f = File::open(path).map_err(|e| FirstLineError::CannotOpenFile {
        name: String::from(path),
        source: e,
    })?;
    let mut buf = BufReader::new(f);
    let mut line = String::new();
    buf.read_line(&mut line)
        .map_err(|e| FirstLineError::NoLines { source: e })?;
    Ok(line)
}

Enter fullscreen mode

Exit fullscreen mode

anyhow doesn’t define the Display trait for us so we have to do that ourselves. Also map_err has to come back if we want to convert error values from one domain to another. But, this time we use Result<String> and we don’t need to define which error is returned.

thiserror crate

This crate makes it easier to define the error type, and can be used in conjunction with anyhow. It uses #[derive(thiserror::Error)] to generate all the Display and std::error::Error boilerplate like other crates do.

But thiserror makes it easier to chain lower-level errors using the #[from] attribute. For example:

#[derive(Error, Debug)]
enum MyError {
    #[error("Everything blew up!")]
    BlewUp,

    #[error(transparent)]
    IoError(#[from] std::io::Error)
}

Enter fullscreen mode

Exit fullscreen mode

This will allow auto-casting from std::io::Error to MyError::IoError.

Let’s look at our demo with anyhow for results, and thiserror for errors:

use std::fs::File;
use std::io::{BufRead, BufReader};

use anyhow::Result;
use thiserror::Error;

#[derive(Debug, Error)]
enum FirstLineError {
    #[error("Cannot open file `{name}` because: {source}")]
    CannotOpenFile {
        name: String,
        source: std::io::Error,
    },
    #[error("Cannot find line in file because: {source}")]
    NoLines {
        source: std::io::Error,
    },
}

fn first_line(path: &str) -> Result<String> {
    let f = File::open(path).map_err(|e| FirstLineError::CannotOpenFile {
        name: String::from(path),
        source: e,
    })?;
    let mut buf = BufReader::new(f);
    let mut line = String::new();
    buf.read_line(&mut line)
        .map_err(|e| FirstLineError::NoLines { source: e })?;
    Ok(line)
}

Enter fullscreen mode

Exit fullscreen mode

Notice the neat embedded field names in the strings on the #[error(...)] lines.

Main function

A quick note on the main function. Rust Edition 2018 as added a feature that allows main to return a Result. If main returns an Err variant, it will return an error code other than 0 to the operating system (signifying a fail condition), and output the error using the Debug trait.

If you wanted the Display trait, then you have to put your main function in another, then have your new main call it and println! the result:

fn main() -> i32 {
    if let Err(e) = run() {
        println!("{}", e);
        return 1;
    }

    return 0;
}

fn run() -> Result<(), Error> { ... }

Enter fullscreen mode

Exit fullscreen mode

If the Debug trait is good enough for printing out your error, you can use:

fn main() -> Result<(), Error> { ...  }

Enter fullscreen mode

Exit fullscreen mode

How does the program know what error code to return? It uses the new Termination trait:

trait Termination {
    fn report(self) -> i32;
}

Enter fullscreen mode

Exit fullscreen mode

The compiler will call report() on the type you return from main.

And there’s much more…

But not this week…

I wanted to talk about the methods on Option and Result, like map_err but this article is already too long. I will cover them next time.

Rust Error Handling Cheatsheet — Result handling functions

Introduction to Rust error handling

Rust error handling is nice but obligatory. Which makes it sometimes plenty of code.

Functions return values of type Result that is «enumeration». In Rust enumeration
means complex value that has alternatives and that alternative is shown with a tag.

Result is defined as Ok or Err. The definition is generic, and both alternatives have
an attribute that can be of any type.

enum Result<T, E> {
    Ok(T),
    Err(E),
}

Normal way to handle errors we use the match statement:

match function_to_do_nice_things() {
  Ok(t) => {
    println!("this went great, {}", t);
  },
  Err(e) => "oops"
}

Method chaining in error handling

If all the errors are wrapped to match statement, we get soon plenty of match
statements inside each other.

One alternative to address this is to chain methods to handle the errors.

value = function_to_do_nice_things()
  .and_then(other_function)
  .map_err(|e| module_error_from_io_error(e))?

Example calls two functions, gets error value from the first that fails, maps it from io error to
our own error and returns it from the function to the caller. If calls are ok, we unwrap the
Ok result and assign it to variable value.

It is pretty difficult to find the right error mapping funtion. There are 21 of them
and the descriptions are pretty cryptic.

Cheatsheet

This cheatsheet lists 21 error Result handling functions and tells what they do.

The division is

  • Six functions that map results to results
  • Two functions that map results to values (or results)
  • Eight functions that can be used to extract Ok value from the Result or to get information on existence of Ok result
  • Four functions that can be used to extract Err value or to get information of existence of Err result
  • One special conversion function

The first column tells what is done to the result if it is Ok variant. The second column tells what is done to the Err variant.
This hopefully makes it easier to find the right function for specific purpose.

For example, if you are looking for a function that leaves Err result as is and maps the Ok result with a function, you quickly
find that map and and_then are such functions. Then you decide if you want to map simply with a function mapping the value (map)
or if you want to return a full Ok/Err result with and_then function.

In these examples

  • r is the result what these functions address
  • t is the Ok value inside r, Ok(t)
  • e is the Err value inside r, Err(e)
  • r2 is the second result given as an argument having t2 and e2
  • f is a function that gets t as input and generates t’
  • F is a function that gets t as input and generates new Result(t’, e’)
  • g is a function that gets e as input and generates e’

Mapping result to result

Ok(t) -> ? Err(e) -> ? Code r: Description
t -> Ok(t’) Unchanged r.map(|t| f(t)) Map ok with function, error as is, mapping can not result error
t -> (t’, e’) Unchanged r.and_then(|t| F(t)) Calls function for Ok value and propagates errors. When you chain these like r.and_then().and_then(), it returns result of last function or the first happened error.
Unchanged _e -> (t’, e’) r.or_else(|_e| F()) In chain r.or_else(f1).or_else(f2) calls functions until one succeeds, does not call after first success, argument must return Result type. Called function gets the error value as argument but likely do not use it.
Unused, return arg r2 (t2, e2) Unchanged r.and(r2) In chain r.and(r2).and(r3) return last ok result or first error
Unchanged Unused, return arg r2 (t2, e2) instead r.or(r2) In chain r.or(r2).or(r3) return value of first Ok or last error, evaluates all or values
Unchanged e -> Err(e’) r.map_err(|e| g(e)) Map error with g(e), that return a normal type that is automatically converted to error result. Map function can not return an error.

Mapping Result to any type

Ok -> ? Err -> ? Code Description
t -> t’ (returned as is) e -> e’ (returned as is) r.map_or_else(|e| g(e), |t| f(t)) Map both Ok and Err with a function. Result can be of any type but it has to be same for both Ok and Error. Err mapping function is first because it is considered as a «default value if normal processing fails» like in the map_or.
t -> t’ (returned as is) Literal (returned as is) r.map_or(literal, |t| f(t)) Map with function. If error, use literal as a default value. Mapping function can return Result but also any other type that matches literal. Note that this does NOT meant that if mapping function fails, use literal. It means that if we can not use mapping function due to error, give the literal instead.

Extract Ok value

Ok -> ? Err -> ? Code Description
t stop function and return Err(e) immediately r? If error, return from the function using this same result. Function result must be compatible.
t panic r.unwrap() Panics with error, may use e as panic message.
t panic with message r.expect("string") unwrap() with a given panic message.
t Literal as t’ r.unwrap_or(literal) Unwrap, if error, use literal from arguments instead.
t e -> t’ r.unwrap_or_else(|e| g(e)) Extract value or derive it from error with function
t Default as t’ r.unwrap_or_default() Returns value or default for that type (if set)
true false r.is_ok() True if ok
Option::Some(t) Option::None r.ok() If Ok, return Option::Some(t), in case of error returns Option::None

Extract error

Ok -> ? Err -> ? Code Description
panic e r.unwrap_err() Panics, may shows value of t
panic e r.expect_err("message") Panics if ok, with set panic message, prints value of t
false true r.is_err() True if error
None Some(e) r.err() Some(e) if error or None if no error

Convert

Ok -> ? Err -> ? Code Description
t -> Some(t) e -> Some(e) r.transpose() Take Option (especially Option::None) out from Result

Question mark operator

To use r?, function must return compatible Result type. For testing,
the main function and tests can return Result type (Rust 2018)

Own errors

It is customary to define your own Error type for your program

pub struct MyError {};
pub type Result<T> = result::Result<T, MyError>;
impl fmt::Display for MyError {
  ..
}
impl fmt::Debug for MyError {
  ..
}

Generating results for testing etc.

let r: Result<u32, String> = Ok(233);
let s: Result<u32, String> = Err("meaningless input");
let t: Result<(), ()> = Ok(());

Понравилась статья? Поделить с друзьями:
  • Rust launcher error loading error startservice failed 1450
  • Rust launcher error launcher network error could not connect to the easyanticheat network
  • Rust has encountered an error and must close как исправить windows 10
  • Rust has encountered an error and must close a crash
  • Rust error to string