Rust Learning Note: Exception Handling

·

6 min read

This article is a summary of Chapter 4.8 of Rust Course (course.rs/)

Type Conversions of Option<T> and Result<T>

or() and and()

or() and and() are similar to the or and and in boolean expressions. They evaluate an Option/Result from two Option/Result values.

or(): returns the first expression that is Some/Ok, returns the second None/Err if both expressions are None/Err.

and(): returns the second Some/Ok if both expressions are Some/Ok, returns the first occurance of None/Err2

fn main() {
    let s1 = Some(1);
    let s2 = Some(2);
    let n: Option<i32> = None;

    let o1: Result<i32, i32> = Ok(1);
    let o2: Result<i32, i32> = Ok(2);
    let e1: Result<i32, i32> = Err(1);
    let e2: Result<i32, i32> = Err(2);

    assert_eq!(s1.or(s2), s1); // Some1 or Some2 = Some1
    assert_eq!(s1.or(n), s1);  // Some or None = Some
    assert_eq!(n.or(s1), s1);  // None or Some = Some
    assert_eq!(n.or(n), n);    // None1 or None2 = None2

    assert_eq!(o1.or(o2), o1); // Ok1 or Ok2 = Ok1
    assert_eq!(o1.or(e1), o1); // Ok or Err = Ok
    assert_eq!(e1.or(o1), o1); // Err or Ok = Ok
    assert_eq!(e1.or(e2), e2); // Err1 or Err2 = Err2

    assert_eq!(s1.and(s2), s2); // Some1 and Some2 = Some2
    assert_eq!(s1.and(n), n);   // Some and None = None
    assert_eq!(n.and(s1), n);   // None and Some = None
    assert_eq!(n.and(n), n);    // None1 and None2 = None1

    assert_eq!(o1.and(o2), o2); // Ok1 and Ok2 = Ok2
    assert_eq!(o1.and(e1), e1); // Ok and Err = Err
    assert_eq!(e1.and(o1), e1); // Err and Ok = Err
    assert_eq!(e1.and(e2), e1); // Err1 and Err2 = Err1
}

or_else() and and_then()

or_else() and and_then() follow the same evaluation rules as or() and and(). However, they take a closure as the second expression.

fn main() {
    let s1 = Some(1);
    let s2 = Some(2);
    let fn_some = || Some(2);
    let n: Option<i32> = None;
    let fn_none = || None;

    assert_eq!(s1.or_else(fn_some), s1);  // Some1 or_else Some2 = Some1
    assert_eq!(s1.or_else(fn_none), s1);  // Some or_else None = Some
    assert_eq!(n.or_else(fn_some), s2);   // None or_else Some = Some
    assert_eq!(n.or_else(fn_none), None); // None1 or_else None2 = None2

    let o1: Result<i32, i32> = Ok(1);
    let o2: Result<i32, i32> = Ok(2);
    let fn_ok = || Ok(2);
    let e1: Result<i32, i32> = Err(1);
    let e2: Result<i32, i32> = Err(2);
    let fn_err = || Err(2);

    assert_eq!(o1.and_then(fn_ok), o2);  // Ok1 and_then Ok2 = Ok2
    assert_eq!(o1.and_then(fn_err), e2); // Ok and_then Err = Err
    assert_eq!(e1.and_then(fn_ok), e1);  // Err and_then Ok = Err
    assert_eq!(e1.and_then(fn_err), e1); // Err1 and_then Err2 = Err1
}

filter

filter is used to filter an Option. It takes a closure that returns a boolean value, and keeps the value if the closure returns true.

fn main() {
    let s1 = Some(3);
    let s2 = Some(6);
    let n = None;

    let fn_is_even = |x: &i8| x % 2 == 0;

    assert_eq!(s1.filter(fn_is_even), n);
    assert_eq!(s2.filter(fn_is_even), s2);
    assert_eq!(n.filter(fn_is_even), n);
}

map() and map_err()

map takes a closure as parameter and maps the value in Some or Ok to another value. It cannot be used to change the value in Err.

fn main() {
    let s1 = Some("abcde");
    let o1: Result<&str, &str> = Ok("abcde");
    let fn_character_count = |s: &str| s.chars().count();

    let e1: Result<&str, &str> = Err("abcde");    
    let e2: Result<usize, &str> = Err("abcde");

    assert_eq!(s1.map(fn_character_count), 5);
    assert_eq!(o1.map(fn_character_count), 5);

    assert_eq!(e1.map(fn_character_count), e2);
}

If we want to map the value in Err to another value, we can use method map_err

fn main() {
    let e1: Result<&str, &str> = Err("404");
    let e2: Result<&str, isize> = Err(404);
    let fn_character_count = |s: &str| -> isize {s.parse().unwrap()};
    assert_eq!(e1.map_err(fn_character_count), e2);
}

map_or provides a default value for map. If the Option/Result is None/Err, the default value is returned. map_or_else is similar to map_or, except that it evaluates the default value with a closure.

fn main() {
    const DEFAULT: u32 = 1;

    let s: Result<u32, ()> = Ok(10);
    let n: Option<u32> = None;
    let fn_closure = |v: u32| v + 2;

    assert_eq!(s.map_or(DEFAULT, fn_closure), 12);
    assert_eq!(n.map_or(DEFAULT, fn_closure), DEFAULT);

    let o = Ok(10);
    let e = Err(5);
    let fn_default = |v: i8| v + 1;
    assert_eq!(o.map_or_else(fn_default, fn_closure), 12);
    assert_eq!(e.map_or_else(fn_default, fn_closure), 6);

Note that in the code above, we are able to use the value in Err in the closure for map_or_else.

ok_or() and ok_or_else()

ok_or and ok_or_else are used to convert an Option to a Result. ok_or takes a a parameter as the default Err value, and ok_or_else takes a closure as Err value.

fn main() {
    const DEFAULT: &str = "error message";
    let s = Some("abcde");
    let n: Option<&str> = None;

    assert_eq!(s.ok_or(DEFAULT), Ok("abcde"));
    assert_eq!(n.ok_or(DEFAULT), Err(DEFAULT));

    let fn_err = || "error message";
    assert_eq!(s.ok_or_else(fn_err), Ok("abcde"));
    assert_eq!(n.ok_or_else(fn_err), Err("error message"));

Defining Custome Error Type

std::error::Error trait allows an object to be represented as error values, i.e., values inside Err() for Result enumerator. A custome errror type needs to implement Debug and Display, which are prerequistes of Error.

use std::fmt;

struct AppError {
    code: usize,
    message: String
}

impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let err_msg = match self.code {
            404 => "Page not found",
            - => "Something went wrong"
        };

        write!(f, "{}", err_msg)
    }
}

impl fmt::Debug for AppError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(
            f,
            "AppError {{ code: {}, message: {} }}",
            self.code, self.message
        )
    }
}

fn produce_error() -> Result<(), AppError> {
    Err(AppError {
        code: 404,
        message: String::from("Page not found")
    })
}

fn main() {
    match produce_error() {
        Err(e) => eprintln!("{}", e),
        _ => println!("No error")
    }
}

Conversion among errors with From trait

From trait allows us to convert one type to another. Using From, we can convert different standard error types to our custome type.

use std::fs::File
use std::io::{self, Read};
use std::num

#[derive(Debug)]
struct AppError {
    kind: String,
    message: String
}

impl From<io::Error> for AppError {
    fn from(error: io:Error) -> Self {
        AppError {
            kind: String::from("io"),
            message: error.to_string()
        }
    }
}

impl From<num::ParseIntError> for AppError {
    fn from(error: num::ParseIntError) -> Self {
        AppError {
            kind: String::from("parse"),
            message: error.to_string()
        }
    }
}

fn main() -> Result<(), AppError> {
    let mut file = File::open("hello world.txt")?;
    let mut content = String::new();
    file.read_to_string(&mut content)?;
    let number: usize;
    number = content.parse()?;

Note that ? is able to implicitly convert the standard error types to our custome type AppError once we implement the From trait for type conversion.

Unifying different error types

Sometimes we want a function to return different types of errors. In this case, we need to have a unified error type for the function type annotation.

One easy way to do that is to use trait object:

use std::fs::read_to_string;
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
    let html = render()?;
    println!("{}", html);
    Ok(())
}

fn render() -> Result<String, Box<dyn Error>> {
    let file = std:;env::var("MARKDOWN")?;
    let source = read_to_string(file)?;
    Ok(source)
}

One limitation of this method is that the trait object would not restrict the specific types of errors. As long as a type implements Debug and Display type, it can be returned as the error. Another way to avoid this is to define custome error types that can be converted from the standard errors:

use std::fs::read_to_string;

fn main() -> Result<(), MyError> {
  let html = render()?;
  println!("{}", html);
  Ok(())
}

fn render() -> Result<String, MyError> {
  let file = std::env::var("MARKDOWN")?;
  let source = read_to_string(file)?;
  Ok(source)
}

#[derive(Debug)]
enum MyError {
  EnvironmentVariableNotFound,
  IOError(std::io::Error),
}

impl From<std::env::VarError> for MyError {
  fn from(_: std::env::VarError) -> Self {
    Self::EnvironmentVariableNotFound
  }
}

impl From<std::io::Error> for MyError {
  fn from(value: std::io::Error) -> Self {
    Self::IOError(value)
  }
}

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

impl std::fmt::Display for MyError {
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    match self {
      MyError::EnvironmentVariableNotFound => write!(f, "Environment variable not found"),
      MyError::IOError(err) => write!(f, "IO Error: {}", err.to_string()),
    }
  }
}

We can utilize third-party libraries to simplify the code above. For example thiserror (https://github.com/dtolnay/thiserror) provides convenien derive macro for Error trait:

use std::fs::read_to_string;

fn main() -> Result<(), MyError> {
  let html = render()?;
  println!("{}", html);
  Ok(())
}

fn render() -> Result<String, MyError> {
  let file = std::env::var("MARKDOWN")?;
  let source = read_to_string(file)?;
  Ok(source)
}

#[derive(thiserror::Error, Debug)]
enum MyError {
    #[error("Environment variable not found")]
    EnvironmentVariableNotFound(#[from] std::env::VarError),
    #[error(transparent)]
    IOError(#[from] std::io::Error)
}

Did you find this article valuable?

Support Raine's blog by becoming a sponsor. Any amount is appreciated!