Skip to content
Go back

Rust Error Handling Best Practices

Third article in the Rust series: Learn how to use Result and Option types in Rust, master the ? operator and custom error types, and write robust error handling code.

Why is Rust’s Error Handling Different?

In most languages, error handling relies on exception mechanisms (try-catch). Rust takes a completely different approach—handling errors through the type system. This means the compiler forces you to handle every possible error case, leaving nothing unhandled.

Two Types of Errors

Rust divides errors into two categories:

panic! — Unrecoverable Errors

When the program encounters a situation it cannot handle, you can use panic! to terminate:

fn divide(a: f64, b: f64) -> f64 {
    if b == 0.0 {
        panic!("Division by zero!");
    }
    a / b
}
rust

In real development, you should try to avoid panic and use Result instead.

The Result Type

Result is Rust’s most commonly used error handling type:

enum Result<T, E> {
    Ok(T),   // Success, contains return value
    Err(E),  // Failure, contains error information
}
rust

Basic Usage

use std::fs;

fn read_config() -> Result<String, std::io::Error> {
    let content = fs::read_to_string("config.toml")?;
    Ok(content)
}

fn main() {
    match read_config() {
        Ok(content) => println!("Config content: {}", content),
        Err(e) => eprintln!("Failed to read config: {}", e),
    }
}
rust

The ? Operator

The ? operator is the essence of Rust error handling—it makes code concise and elegant:

The Option Type

Option is used to represent a value that may or may not exist:

enum Option<T> {
    Some(T), // Has value
    None,    // No value
}
rust

Custom Error Types

In real projects, we usually need custom error types:

Simplifying with thiserror

In real projects, it’s recommended to use the thiserror crate to simplify error definitions:

Error Handling Best Practices

1. Library Code Should Return Result

pub fn parse_config(input: &str) -> Result<Config, ConfigError> {
    // Don't panic in library code
    // Let the caller decide how to handle errors
    todo!()
}
rust

2. Use anyhow to Simplify Application-Level Error Handling

use anyhow::{Context, Result};

fn main() -> Result<()> {
    let config = std::fs::read_to_string("config.toml")
        .context("Failed to read config file")?;

    let port: u16 = config
        .parse()
        .context("Failed to parse port number")?;

    println!("Server starting on port {}", port);
    Ok(())
}
rust

3. Use unwrap Wisely

// Only use unwrap/expect in these cases:
// 1. In test code
#[test]
fn test_parse() {
    let result = parse("valid input").unwrap();
    assert_eq!(result, expected);
}

// 2. When certain it won't fail (use expect with a reason)
let home = std::env::var("HOME")
    .expect("HOME environment variable must be set");
rust

Summary

Rust’s error handling mechanism requires more upfront investment, but it ensures your code won’t have unhandled errors. The combination of Result + ? operator makes error handling both safe and elegant. In the next article, we’ll explore Rust’s concurrency programming model.

Previous: Understanding Rust’s Ownership System

Next: Rust Concurrency in Practice



Previous Post
Understanding Rust's Ownership System
Next Post
Rust Concurrency in Practice

评论区

文明评论,共建和谐社区