Loading...
Loading...

Rust Error Handling

Error handling in Rust is fundamentally different from exception-based languages. Rust forces explicit handling of potential errors through its Result and Option types, catching mistakes at compile time rather than runtime. This tutorial explains each concept clearly with practical examples.

1. The Result Type

What it is: Rust's primary error handling mechanism. Result<T, E> is an enum with two variants: Ok(T) for success and Err(E) for failure.

Why it matters: Unlike exceptions, Results must be explicitly handled, making error paths visible in your code.

fn divide(numerator: f64, denominator: f64) -> Result<f64, String> {
    if denominator == 0.0 {
        Err("Cannot divide by zero".to_string())
    } else {
        Ok(numerator / denominator)
    }
}

// Usage example:
let result = divide(10.0, 0.0);
match result {
    Ok(value) => println!("Result: {}", value),
    Err(e) => println!("Error occurred: {}", e),
}

Key points:

  • Must handle both Ok and Err cases
  • Return type clearly indicates possible failure
  • No hidden control flow like try/catch blocks

2. The ? Operator

What it does: The ? operator automatically propagates errors up the call stack if they occur, or unwraps the success value.

When to use: When you want to delegate error handling to the caller rather than handling it immediately.

use std::fs;

fn read_config() -> Result<String, std::io::Error> {
    // ? automatically returns Err if file operations fail
    let config = fs::read_to_string("config.toml")?;
    Ok(config)
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let config = read_config()?; // Propagates error to main
    println!("Config: {}", config);
    Ok(())
}

Key behaviors:

  • Works in functions returning Result or Option
  • Performs automatic error conversion if From trait is implemented
  • Makes error handling concise without losing clarity

3. Creating Custom Errors

Why needed: Real applications need domain-specific error types with contextual information.

Best practice: Implement the standard Error trait for compatibility with Rust's error ecosystem.

use std::fmt;

#[derive(Debug)]
enum DatabaseError {
    ConnectionFailed,
    QueryFailed(String), // Contains error details
    InvalidData { field: String, reason: String },
}

impl fmt::Display for DatabaseError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Self::ConnectionFailed => write!(f, "Failed to connect to database"),
            Self::QueryFailed(msg) => write!(f, "Query failed: {}", msg),
            Self::InvalidData { field, reason } => 
                write!(f, "Invalid data in {}: {}", field, reason),
        }
    }
}

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

fn query_database() -> Result<(), DatabaseError> {
    Err(DatabaseError::QueryFailed("Syntax error".to_string()))
}

Key components:

  • Debug derivation for error reporting
  • Display implementation for user-facing messages
  • Error trait implementation for compatibility

4. Using Error Handling Crates

thiserror: For defining your own error types with minimal boilerplate.

anyhow: For application code where precise error types aren't critical.

// thiserror example (library code)
#[derive(thiserror::Error, Debug)]
pub enum ConfigError {
    #[error("missing configuration file at {0}")]
    MissingFile(String),
    #[error("invalid config format: {0}")]
    InvalidFormat(#[from] toml::de::Error),
}

// anyhow example (application code)
use anyhow::{Context, Result};

fn load_config() -> Result<Config> {
    let path = "config.toml";
    let data = std::fs::read_to_string(path)
        .with_context(|| format!("Failed to read {}", path))?;
    toml::from_str(&data).context("Failed to parse config")
}

When to use each:

  • thiserror: When creating library APIs that expose specific error types
  • anyhow: For application code where exact error types matter less
  • eyre/snafu: Alternatives with different reporting styles

5. Choosing Between Option and Result

Option: Use when something may legitimately be absent (no error occurred).

Result: Use when an operation might fail and you need to explain why.

// Option example - finding an element
fn find_user(id: u32) -> Option<User> {
    users.iter().find(|u| u.id == id).cloned()
}

// Result example - parsing input
fn parse_id(input: &str) -> Result<u32, ParseIntError> {
    input.parse()
}

// Converting between them
fn get_user_age(input: &str) -> Result<u8, String> {
    let id = parse_id(input)?;
    find_user(id)
        .map(|u| u.age)
        .ok_or_else(|| "User not found".to_string())
}

Conversion methods:

  • ok_or()/ok_or_else(): Convert Option to Result
  • ok(): Convert Result to Option (discarding error)
  • transpose(): Convert Option<Result> to Result<Option>

6. When to Panic

Rule of thumb: Panic for programming errors, use Result for expected failures.

Good panic cases:

  • Contract violations (invalid API usage)
  • Unrecoverable system state
  • Tests and prototypes
// Good panic example
fn setup(config: &Config) {
    if config.api_key.is_empty() {
        panic!("API key must be configured");
    }
    // ...
}

// Better alternative for libraries
fn setup(config: &Config) -> Result<(), SetupError> {
    if config.api_key.is_empty() {
        return Err(SetupError::MissingApiKey);
    }
    Ok(())
}

Custom panic hooks:

// In main()
std::panic::set_hook(Box::new(|panic_info| {
    eprintln!("Critical failure: {}", panic_info);
}));
0 Interaction
0 Views
Views
0 Likes
×
×
×
🍪 CookieConsent@Ptutorials:~

Welcome to Ptutorials

$ Allow cookies on this site ? (y/n)

top-home