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 the
panic!`panic!` macro to terminate the program. - Recoverable errors(Recoverable): operations may fail but can be handled, using the
Result<T, E>`Result<T, E>` enum.
This design forces developers to handle possible errors at compile time, rather than discovering omissions at runtime.
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:
- Print the error message and where it occurred
- Unwind the call stack and clean up resources
- Terminate the program
Example
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 Use
panic!: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<T, E> {
Ok(T), // 操作成功,包含结果值
Err(E), // 操作失败,包含错误信息
}
All functions in Rust's standard library that may fail return `Result<T, E>`.ResultFor example, opening a file:
2.1 Using match to Handle Result
Example
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
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
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::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::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<String, io::Error> {
let mut content = String::new();
File::open(path)?.read_to_string(&mut content)?;
Ok(content)
}
?How the `?` operator works:
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::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::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::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 use
thiserrorthe 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<T> {
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
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 |
Other extensionsRust'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.