Rust Error Handling

Rust adopts a unique error handling mechanism,with no exceptions and no try/catch.It divides errors into two categories and handles them in different ways:

  • Unrecoverable errors(Unrecoverable): when there is a serious problem in program logic, use thepanic!`panic!` macro to terminate the program.
  • Recoverable errors(Recoverable): operations may fail but can be handled, using theResult<T, E>`Result<T, E>` enum.

This design forces developers to handle possible errors at compile time, rather than discovering omissions at runtime.

Rust Error Handling Mechanism Overview Errors in Programs Unrecoverable errors panic!("serious error") Print error message → Unwind call stack → Terminate program Examples: array out of bounds, division by zero, logic bugs → Similar to uncaught exceptions in other languages Recoverable errors Result<T, E> Ok(T) represents success, Err(E) represents failure Examples: file not found, network timeout, parsing failure → Similar to checked exceptions in other languages

1. Unrecoverable Errors: panic!

panic!The `panic!` macro is used to indicate that the program encountered a serious error that prevents it from continuing. When invoked, it will:

  1. Print the error message and where it occurred
  2. Unwind the call stack and clean up resources
  3. Terminate the program

Example

fn main() {
    panic!("A serious error occurred");
    // The following code will never execute
    println!("Hello, Rust");
}

Output:

thread 'main' panicked at '发生了严重错误', src/main.rs:2:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace.

The output has two lines of information:

  • First line: the location of the panic (file name and line number) and the error message
  • Second line: a hint on how to view the full call stack backtrace

Viewing the Call Stack Backtrace

Set theRUST_BACKTRACE=1`RUST_BACKTRACE` environment variable to see the full call stack, which helps locate the root cause of the panic:

# Linux / macOS
RUST_BACKTRACE=1 cargo run

# Windows PowerShell
$env:RUST_BACKTRACE=1; cargo run

The backtrace output will list the complete call chain from the panic location to themain`main` function. This information is very useful when the panic occurs in a deep call.

When to Usepanic!:There are logical errors (bugs) in the code that should not happen, such as out-of-bounds access, dereferencing a null pointer, or violating an immutable contract. For predictable errors caused by external input, you should use `Result<T, E>`.Result。


2. Recoverable Errors: Result<T, E>

Result`Result<T, E>` is the enum in Rust's standard library used to represent operations that may fail:

enum Result&lt;T, E&gt; {
    Ok(T),    // 操作成功,包含结果值
    Err(E),   // 操作失败,包含错误信息
}

All functions in Rust's standard library that may fail return `Result<T, E>`.ResultFor example, opening a file:

`File::open()` returns `Result<File, io::Error>` let f = File::open("hello.txt"); File exists Ok(file) Gets a `File` object, which can be read from and written to File does not exist Err(io::Error) Gets error information, which can be handled

2.1 Using match to Handle Result

Example

use std::fs::File;

fn main() {
    let f = File::open("hello.txt");

    match f {
        Ok(file) => {
            println!("File opened successfully: {:?}", file);
        }
        Err(error) => {
            println!("Failed to open file: {}", error);
        }
    }
}

2.2 Using if let for Simplified Handling

When you only care about the success case,if letComparematch`if let` is more concise:

Example

use std::fs::File;

fn main() {
    let f = File::open("hello.txt");

    if let Ok(file) = f {
        println!("File opened successfully");
        // Use file here ...
    } else {
        println!("Failed to open file");
    }
}

2.3 unwrap and expect: Quick but Dangerous

If you are sure the operation will not fail (or you don't want to handle errors during prototyping), you can use these two shortcut methods:

Method Behavior Panic message on failure
.unwrap() On success, returns the value; on failure, it directlyTpanicspanic! using the default error message.
.expect("msg") On success, returns the value; on failure, it directlyTpanicspanic! using a custom error message (easier to debug).

Example

use std::fs::File;

fn main() {
    // unwrap: panics on failure, uses the default message
    // thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: ...'
    let f1 = File::open("hello.txt").unwrap();

    // expect: panics on failure, uses a custom message (recommended)
    // thread 'main' panicked at 'Failed to open config file: ...'
    let f2 = File::open("hello.txt").expect("Failed to open config file");
}

Recommendation:In production code, prefer using `expect`expectrather than `unwrap`,unwrapbecause a custom error message helps you quickly locate the problem when a panic occurs. An even better approach is to use the?`?` operator to propagate the error to the caller.


3. Error Propagation: The ? Operator

In real development, when a function encounters an error, it often does not want to handle it itself, but instead wants topropagatethe error to the caller. Rust provides the?`?` operator to simplify this.

3.1 Manual Propagation vs. the ? Operator

First, take a look at manual propagation—verbose but clear:

Example

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

// Manually propagating errors (tedious)
fn read_file_manual(path: &str) -> Result<String, io::Error> {
    let f = File::open(path);

    // Return Err if opening fails
    let mut file = match f {
        Ok(file) => file,
        Err(e) => return Err(e),  // Return the error early
    };

    let mut content = String::new();

    // Return Err if reading fails
    match file.read_to_string(&mut content) {
        Ok(_) => Ok(content),
        Err(e) => Err(e),
    }
}

Using the?`?` operator, the same logic can be simplified to:

Example

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

// Using the ? operator (concise)
fn read_file(path: &str) -> Result<String, io::Error> {
    let mut file = File::open(path)?;   // Automatically returns Err on failure
    let mut content = String::new();
    file.read_to_string(&mut content)?; // Automatically returns Err on failure
    Ok(content)
}

You can also chain calls to simplify it further:

fn read_file(path: &str) -> Result&lt;String, io::Error&gt; {
    let mut content = String::new();
    File::open(path)?.read_to_string(&mut content)?;
    Ok(content)
}

?How the `?` operator works:

Workflow of the ? operator let val = some_operation()?; Ok or Err? Result Ok `val` = value inside `Ok` Continue executing the subsequent code Err `return Err(e)` returns early

Important limitation:?The `?` operator can only be used in functions that returnResult`Result` (orOption`Option`). Since Rust 1.39,mainthe `main` function can also return a `Result`.Result。

3.2 Using ? in main

By default, themain`main` function returns()`()`, so `?` cannot be used.?But you can makemain`main` return a `Result`.Result:

Example

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

fn read_file(path: &str) -> Result<String, io::Error> {
    let mut content = String::new();
    File::open(path)?.read_to_string(&mut content)?;
    Ok(content)
}

// main returns a Result, so ? can be used in main
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let content = read_file("hello.txt")?;  // ? can also be used in main now
    println!("{}", content);
    Ok(())
}

4. Custom Error Types and Categorized Handling

In real projects, you usually need different types of errors to be handled differently. Rust achieves this through thekind()`kind()` method:

Example

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

// Wrap file reading into a separate function and propagate errors with ?
fn read_text_from_file(path: &str) -> Result<String, io::Error> {
    let mut f = File::open(path)?;
    let mut s = String::new();
    f.read_to_string(&mut s)?;
    Ok(s)
}

fn main() {
    match read_text_from_file("hello.txt") {
        Ok(content) => println!("File content: "\n{}", content),
        Err(e) => {
            // Handle different cases based on the error type
            match e.kind() {
                io::ErrorKind::NotFound => {
                    println!("File not found, please check the path");
                }
                io::ErrorKind::PermissionDenied => {
                    println!("No permission to read the file");
                }
                _ => {
                    println!("An error occurred while reading the file: {}", e);
                }
            }
        }
    }
}

Output (when the file does not exist):

File does not exist, please check the path.

io::ErrorKindCommon variants:

ErrorKind Meaning
NotFound File or directory does not exist
PermissionDenied Insufficient permissions
AlreadyExists File already exists (when creating)
ConnectionRefused Connection refused
TimedOut Operation timed out
InvalidInput Invalid parameter

5. Custom Error Types

In projects, you usually need to define your own error types to represent errors in business logic:

Example

use std::fmt;
use std::num::ParseIntError;

// Define custom error enum
#[derive(Debug)]
enum AppError {
    IoError(std::io::Error),
    ParseError(ParseIntError),
    CustomError(String),
}

// Implement Display trait for formatted output
impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            AppError::IoError(e) => write!(f, "IO error: {}", e),
            AppError::ParseError(e) => write!(f, "Parse error: {}", e),
            AppError::CustomError(msg) => write!(f, "Business error: {}", msg),
        }
    }
}

// Implement From trait so the ? operator automatically converts error types
impl From<std::io::Error> for AppError {
    fn from(error: std::io::Error) -> Self {
        AppError::IoError(error)
    }
}

impl From<ParseIntError> for AppError {
    fn from(error: ParseIntError) -> Self {
        AppError::ParseError(error)
    }
}

// Now you can use ? in the same function to handle different error types
fn process_config(path: &str) -> Result<i32, AppError> {
    let content = std::fs::read_to_string(path)?; // io::Error → AppError
    let value: i32 = content.trim().parse()?;      // ParseIntError → AppError

    if value < 0 {
        return Err(AppError::CustomError("Configuration value cannot be negative".into()));
    }

    Ok(value)
}

fn main() {
    match process_config("config.txt") {
        Ok(val) => println!("Configuration value: {}", val),
        Err(e) => println!("Error: {}", e),
    }
}

Recommended third-party libraries:In real projects, you can usethiserrorthe library to automatically deriveDisplayandFromimplementations, greatly reducing boilerplate code. For application-layer code,anyhowthe library provides convenientanyhow::Resulttypes, suitable for rapid development.


6. Option<T>: A Value That May Not Exist

BesidesResult", Rust also has another important enum for handling the \"value may not exist\" case—"Option<T>:

enum Option&lt;T&gt; {
    Some(T),  // 有值
    None,     // 没有值
}

Optionused to replace `null` in other languagesnull". Rust has no `null`,"null", any value that may be empty must be"Option"wrapped in `Option`:"

Example

fn find_user(id: u32) -> Option<String> {
    match id {
        1 => Some("Alice".to_string()),
        2 => Some("Bob".to_string()),
        _ => None, // User does not exist
    }
}

fn main() {
    // Use match to handle Option
    match find_user(1) {
        Some(name) => println!("Found user: {}", name),
        None => println!("User does not exist"),
    }

    // Use if let to simplify
    if let Some(name) = find_user(99) {
        println!("Found user: {}", name);
    } else {
        println!("User does not exist");
    }

    // unwrap_or provides a default value
    let name = find_user(99).unwrap_or("Anonymous user".to_string());
    println!("Username: {}", name);  // Anonymous user

    // The ? operator also works with Option
    let first_char = get_first_char("hello");
    println!("First letter: {:?}", first_char);  // Some('h')
}

fn get_first_char(s: &str) -> Option<char> {
    s.chars().next() // Returns Option<char>
}

OptionandResultComparison:

Comparison Item Option<T> Result<T, E>
Purpose Value may exist or not exist Operation may succeed or fail
Success Some(T) Ok(T)
Failure None(no additional information) Err(E)(contains error reason)
Typical scenarios Lookup, optional fields, default values File operations, network requests, parsing
Conversion ok_or(err) → Result ok() → Option

Summary

Scenario Recommended approach Description
Program encounters an unrecoverable bug panic!("原因") Terminate the program, for situations that should not happen
Operation may fail ReturnResult<T, E> Force caller to handle errors
Propagate errors within a function ?operator Automatically return Err on failure, extract value on success
Quick prototyping / testing .expect("原因") Panic on failure, but with a clear error message
Value may not exist Option<T> useSome / NoneReplaces null
Handle separately by error type e.kind() Match specific error variants
Custom error types ImplementDisplay + From Together with?Implement automatic conversion

Rust's error handling philosophy:Errors are part of the type system, not exceptions in control flow. The compiler forces you to handle every possible error case, which makes your program more reliable at runtime.

Other extensions