Mastering Error Handling in Rust: Beyond Result and Option

Error handling in Rust is not as simple as just using Result and Option. For beginners, Rust’s error handling can be quite unfriendly. After struggling with it multiple times, I decided to organize my knowledge on the topic. This guide consists of two main parts: Official methods provided for working with Result How to define and handle custom errors. Mastering these concepts will help you overcome the challenges of Rust’s error handling. Methods for Error Handling To handle errors effectively, you need to make use of Rust's built-in methods. This will make your work much easier. Some useful methods include: or() and() or_else() and_then() map() map_err() map_or() map_or_else() ok_or() ok_or_else() ... Below, I’ll explain when to use these methods, how to use them, and ultimately, how to design your Err types when writing code. or() and and() These methods allow you to choose between two options, similar to logical OR and AND. or(): Evaluates expressions in order. If any expression results in Some or Ok, that value is immediately returned. and(): Returns the value from the second expression if both are Some or Ok. If either result is None or Err, it returns that instead. let s1 = Some("some1"); let s2 = Some("some2"); let n: Option = None; let o1: Result = Ok("ok1"); let o2: Result = Ok("ok2"); let e1: Result = Err("error1"); let e2: Result = Err("error2"); 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() The or() and and() methods only choose between two values, without modifying them. If you need to apply more complex logic, you should use closures with or_else() and and_then(). // Using or_else() with Option let s1 = Some("some1"); let s2 = Some("some2"); let fn_some = || Some("some2"); // Equivalent to: let fn_some = || -> Option { Some("some2") }; let n: Option = 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 // Using or_else() with Result let o1: Result = Ok("ok1"); let o2: Result = Ok("ok2"); let fn_ok = |_| Ok("ok2"); let e1: Result = Err("error1"); let e2: Result = Err("error2"); let fn_err = |_| Err("error2"); assert_eq!(o1.or_else(fn_ok), o1); // Ok1 or_else Ok2 = Ok1 assert_eq!(o1.or_else(fn_err), o1); // Ok or_else Err = Ok assert_eq!(e1.or_else(fn_ok), o2); // Err or_else Ok = Ok assert_eq!(e1.or_else(fn_err), e2); // Err1 or_else Err2 = Err2 map() If you want to modify the value inside Result or Option, use map(). let s1 = Some("abcde"); let s2 = Some(5); let n1: Option = None; let n2: Option = None; let o1: Result = Ok("abcde"); let o2: Result = Ok(5); let e1: Result = Err("abcde"); let e2: Result = Err("abcde"); let fn_character_count = |s: &str| s.chars().count(); assert_eq!(s1.map(fn_character_count), s2); // Some1 map = Some2 assert_eq!(n1.map(fn_character_count), n2); // None1 map = None2 assert_eq!(o1.map(fn_character_count), o2); // Ok1 map = Ok2 assert_eq!(e1.map(fn_character_count), e2); // Err1 map = Err2 map_err() If you need to modify the Err value in Result, use map_err(). let o1: Result = Ok("abcde"); let o2: Result = Ok("abcde"); let e1: Result = Err("404"); let e2: Result = Err(404); let fn_character_count = |s: &str| -> isize { s.parse().unwrap() }; assert_eq!(o1.map_err(fn_character_count), o2); // Ok1 map = Ok2 assert_eq!(e1.map_err(fn_character_count), e2); // Err1 map = Err2 map_or() If you are sure that there will be no Err, you can use map_or() to return a default value instead. const V_DEFAULT: u32 = 1; let s: Result = Ok(10); let n: Option = None; let fn_closure = |v: u32| v + 2; assert_eq!(s.map_or(V_DEFAULT, fn_closure), 12); assert_eq!(n.map_or(V_DEFAULT, fn_closure), V_DEFAULT); map_or_else() map_or() only allows returning a default value, but if you need a closure to provide the default, use map_or_else(). let s = Some(10); let n: Option = None; let fn_closure = |v: i8| v + 2; let fn_de

Mar 13, 2025 - 22:45
 0
Mastering Error Handling in Rust: Beyond Result and Option

Cover

Error handling in Rust is not as simple as just using Result and Option. For beginners, Rust’s error handling can be quite unfriendly. After struggling with it multiple times, I decided to organize my knowledge on the topic. This guide consists of two main parts:

  1. Official methods provided for working with Result
  2. How to define and handle custom errors.

Mastering these concepts will help you overcome the challenges of Rust’s error handling.

Methods for Error Handling

To handle errors effectively, you need to make use of Rust's built-in methods. This will make your work much easier.

Some useful methods include:

  • or()
  • and()
  • or_else()
  • and_then()
  • map()
  • map_err()
  • map_or()
  • map_or_else()
  • ok_or()
  • ok_or_else()
  • ...

Below, I’ll explain when to use these methods, how to use them, and ultimately, how to design your Err types when writing code.

or() and and()

These methods allow you to choose between two options, similar to logical OR and AND.

  • or(): Evaluates expressions in order. If any expression results in Some or Ok, that value is immediately returned.
  • and(): Returns the value from the second expression if both are Some or Ok. If either result is None or Err, it returns that instead.
let s1 = Some("some1");
let s2 = Some("some2");
let n: Option<&str> = None;

let o1: Result<&str, &str> = Ok("ok1");
let o2: Result<&str, &str> = Ok("ok2");
let e1: Result<&str, &str> = Err("error1");
let e2: Result<&str, &str> = Err("error2");

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()

The or() and and() methods only choose between two values, without modifying them. If you need to apply more complex logic, you should use closures with or_else() and and_then().

// Using or_else() with Option
let s1 = Some("some1");
let s2 = Some("some2");
let fn_some = || Some("some2");  // Equivalent to: let fn_some = || -> Option<&str> { Some("some2") };

let n: Option<&str> = 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

// Using or_else() with Result
let o1: Result<&str, &str> = Ok("ok1");
let o2: Result<&str, &str> = Ok("ok2");
let fn_ok = |_| Ok("ok2");

let e1: Result<&str, &str> = Err("error1");
let e2: Result<&str, &str> = Err("error2");
let fn_err = |_| Err("error2");

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

map()

If you want to modify the value inside Result or Option, use map().

let s1 = Some("abcde");
let s2 = Some(5);

let n1: Option<&str> = None;
let n2: Option<usize> = None;

let o1: Result<&str, &str> = Ok("abcde");
let o2: Result<usize, &str> = Ok(5);

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

let fn_character_count = |s: &str| s.chars().count();

assert_eq!(s1.map(fn_character_count), s2); // Some1 map = Some2
assert_eq!(n1.map(fn_character_count), n2); // None1 map = None2

assert_eq!(o1.map(fn_character_count), o2); // Ok1 map = Ok2
assert_eq!(e1.map(fn_character_count), e2); // Err1 map = Err2

map_err()

If you need to modify the Err value in Result, use map_err().

let o1: Result<&str, &str> = Ok("abcde");
let o2: Result<&str, isize> = Ok("abcde");

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!(o1.map_err(fn_character_count), o2); // Ok1 map = Ok2
assert_eq!(e1.map_err(fn_character_count), e2); // Err1 map = Err2

map_or()

If you are sure that there will be no Err, you can use map_or() to return a default value instead.

const V_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(V_DEFAULT, fn_closure), 12);
assert_eq!(n.map_or(V_DEFAULT, fn_closure), V_DEFAULT);

map_or_else()

map_or() only allows returning a default value, but if you need a closure to provide the default, use map_or_else().

let s = Some(10);
let n: Option<i8> = None;

let fn_closure = |v: i8| v + 2;
let fn_default = || 1;

assert_eq!(s.map_or_else(fn_default, fn_closure), 12);
assert_eq!(n.map_or_else(fn_default, fn_closure), 1);

ok_or()

If you want to convert an Option into a Result, you can use ok_or().

const ERR_DEFAULT: &str = "error message";

let s = Some("abcde");
let n: Option<&str> = None;

let o: Result<&str, &str> = Ok("abcde");
let e: Result<&str, &str> = Err(ERR_DEFAULT);

assert_eq!(s.ok_or(ERR_DEFAULT), o); // Some(T) -> Ok(T)
assert_eq!(n.ok_or(ERR_DEFAULT), e); // None -> Err(default)

ok_or_else()

When dealing with potential Err cases and wanting to return an error of the same type using a closure, use ok_or_else().

let s = Some("abcde");
let n: Option<&str> = None;
let fn_err_message = || "error message";

let o: Result<&str, &str> = Ok("abcde");
let e: Result<&str, &str> = Err("error message");

assert_eq!(s.ok_or_else(fn_err_message), o); // Some(T) -> Ok(T)
assert_eq!(n.ok_or_else(fn_err_message), e); // None -> Err(default)

How to Design Errors

Beginners often get frustrated with Rust's strict error types, especially when facing type mismatches in multiple Result returns. By understanding Result types more deeply, you can avoid these frustrations.

Defining Simple Custom Errors

When writing programs, it's common to define custom errors. Here's an example of a simple custom Result:

use std::fmt;

// CustomError is a user-defined error type.
#[derive(Debug)]
struct CustomError;

// Implementing the Display trait for user-facing error messages.
impl fmt::Display for CustomError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "An Error Occurred, Please Try Again!")
    }
}

// Example function that generates a CustomError.
fn make_error() -> Result<(), CustomError> {
    Err(CustomError)
}

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

    eprintln!("{:?}", make_error());
}

Note: The eprintln! macro is used for error output, but functions the same as println! unless the output is redirected.

By implementing Debug and Display, not only can you format errors for display, but you can also convert custom errors into Box trait objects.

Defining More Complex Errors

In real-world scenarios, we often assign error codes and messages:

use std::fmt;

struct CustomError {
    code: usize,
    message: String,
}

// Display different error messages based on the code.
impl fmt::Display for CustomError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        let err_msg = match self.code {
            404 => "Sorry, Cannot find the Page!",
            _ => "Sorry, something is wrong! Please Try Again!",
        };
        write!(f, "{}", err_msg)
    }
}

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

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

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

    eprintln!("{:?}", make_error());
}

Manual implementation of Display and Debug allows for more customized output than using #[derive(Debug)].

Error Conversion

What if you're using third-party libraries, each defining its own error types? Rust provides the std::convert::From trait for error conversion.

use std::fs::File;
use std::io;

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

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

fn main() -> Result<(), CustomError> {
   let _file = File::open("nonexistent_file.txt")?;
   Ok(())
}

The ? operator automatically converts std::io::Error into CustomError. This approach simplifies error handling considerably.

Handling Multiple Error Types

What if your function deals with multiple error types?

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

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

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

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

fn main() -> Result<(), CustomError> {
    let mut file = File::open("hello_world.txt")?;
    let mut content = String::new();
    file.read_to_string(&mut content)?;
    let _number: usize = content.parse()?;
    Ok(())
}

Advanced Error Handling Strategies

When functions return different error types, here are four common approaches:

1. Using Box

This method converts all error types into a 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)
}

Pros: Simplifies code.

Cons: Slight performance loss and potential loss of error type information.

2. Custom Error Types

Define an enum to represent all error types.

#[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)
    }
}

Cons: Verbose but provides precise error control.

3. Using thiserror

Simplifies custom error definitions with annotations.

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

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

Highly recommended for a balance of simplicity and control.

4. Using anyhow

Encapsulates any error type, offering convenience at the cost of performance.

use anyhow::Result;

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

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

Final Thoughts: Just Do It!

With these techniques, you're now equipped to handle error management in Rust. Whether you prefer simplicity or fine control, Rust's error handling mechanisms can adapt to your needs.

We are Leapcell, your top choice for hosting Rust projects.

Leapcell

Leapcell is the Next-Gen Serverless Platform for Web Hosting, Async Tasks, and Redis:

Multi-Language Support

  • Develop with Node.js, Python, Go, or Rust.

Deploy unlimited projects for free

  • pay only for usage — no requests, no charges.

Unbeatable Cost Efficiency

  • Pay-as-you-go with no idle charges.
  • Example: $25 supports 6.94M requests at a 60ms average response time.

Streamlined Developer Experience

  • Intuitive UI for effortless setup.
  • Fully automated CI/CD pipelines and GitOps integration.
  • Real-time metrics and logging for actionable insights.

Effortless Scalability and High Performance

  • Auto-scaling to handle high concurrency with ease.
  • Zero operational overhead — just focus on building.

Explore more in the Documentation!

Try Leapcell

Follow us on X: @LeapcellHQ

Read on our blog