DEV Community

Cover image for Mastering Error Handling in Rust: Beyond Result and Option
Leapcell
Leapcell

Posted on

14 3 3 2 2

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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());
}
Enter fullscreen mode Exit fullscreen mode

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<dyn std::error::Error> 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());
}
Enter fullscreen mode Exit fullscreen mode

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(())
}
Enter fullscreen mode Exit fullscreen mode

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(())
}
Enter fullscreen mode Exit fullscreen mode

Advanced Error Handling Strategies

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

1. Using Box<dyn Error>

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)
}
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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),
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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

Hostinger image

Get n8n VPS hosting 3x cheaper than a cloud solution

Get fast, easy, secure n8n VPS hosting from $4.99/mo at Hostinger. Automate any workflow using a pre-installed n8n application and no-code customization.

Start now

Top comments (1)

Collapse
 
orunsolu profile image
Dru

I just finished reading your article, and I must say, it’s incredibly rich and resourceful! Thank you for sharing such valuable content.

👋 Kindness is contagious

Explore a trove of insights in this engaging article, celebrated within our welcoming DEV Community. Developers from every background are invited to join and enhance our shared wisdom.

A genuine "thank you" can truly uplift someone’s day. Feel free to express your gratitude in the comments below!

On DEV, our collective exchange of knowledge lightens the road ahead and strengthens our community bonds. Found something valuable here? A small thank you to the author can make a big difference.

Okay