Skip to content
DocsRust LearningintermediateError Handling
Chapter 8 of 19·intermediate·7 min read

Error Handling

Xử Lý Lỗi

Result, Option, and the ? operator

Hover or tap any paragraph to see Vietnamese translation

The panic! Macro and When to Use It

When Rust encounters a situation it cannot recover from — such as an out-of-bounds array access — it panics. You can also trigger a panic manually with the panic! macro. When a program panics it prints an error message, unwinds the stack, and exits.

panics.rs
1fn main() {2    // Explicit panic with a message3    // panic!("Something went terribly wrong");45    // Index out of bounds causes a panic automatically6    let v = vec![1, 2, 3];7    // v[99]; // thread 'main' panicked at 'index out of bounds'89    // RUST_BACKTRACE=1 cargo run  → shows full stack trace10}1112fn only_positive(n: i32) -> i32 {13    if n <= 0 {14        panic!("Expected a positive number, got {n}");15    }16    n17}

When to Use panic

  • In examples and prototypes where error handling would obscure the main point
  • In tests — panicking is how tests fail
  • When state is genuinely unrecoverable and continuing would cause worse corruption
  • When a contract violation occurs (precondition not met by the caller)
Warning
Do not use panic for normal control flow. Any recoverable error should use Result<T, E>.

The Result<T, E> Type

Result is the standard return type for operations that can fail in Rust. It is an enum with two variants: Ok(T) for success and Err(E) for failure. The compiler forces you to handle both cases.

result.rs
1use std::fs;2use std::io;3use std::num::ParseIntError;45// Result<T, E> is defined as:6// enum Result<T, E> {7//     Ok(T),8//     Err(E),9// }1011fn read_username(path: &str) -> Result<String, io::Error> {12    fs::read_to_string(path)

T is the type of the success value, E is the type of the error. Both are generic parameters, so Result can represent any success/failure pair.

unwrap() and expect()

These two convenience methods extract the value inside Ok, or panic if the result is Err. They are useful in prototypes and quick scripts but should be avoided in production code.

unwrap_expect.rs
1use std::fs;23fn main() {4    // unwrap() panics with a generic message on Err5    let content = fs::read_to_string("data.txt").unwrap();67    // expect() lets you provide a custom panic message — prefer this over unwrap8    let content = fs::read_to_string("data.txt")9        .expect("data.txt should exist and be readable");1011    // On Option types too12    let v = vec![1, 2, 3];13    let first = v.first().unwrap();           // panics if empty14    let first = v.first().expect("vec was empty"); // clearer panic15    println!("{first}");16}
Tip
Prefer expect over unwrap — your message should explain why the error should not occur, making debugging easier when it does.

The ? Operator for Error Propagation

The ? operator is syntactic sugar for propagating errors up the call chain. If the value is Ok(x), it unwraps to x. If it is Err(e), it converts e to the enclosing function's error type and returns early.

question_mark.rs
1use std::fs;2use std::io;3use std::io::Read;45// Without ? — verbose6fn read_name_verbose(path: &str) -> Result<String, io::Error> {7    let mut file = match fs::File::open(path) {8        Ok(f) => f,9        Err(e) => return Err(e),10    };11    let mut s = String::new();12    match file.read_to_string(&mut s) {

The ? operator calls From::from on the error to convert the error type if needed. This allows your function to return a single error type even when inner operations produce different error types.

Custom Error Types

For library code you should define your own error type. This lets users of your library pattern-match on specific error kinds and handle them appropriately.

custom_error.rs
1use std::fmt;2use std::num::ParseIntError;34#[derive(Debug)]5enum AppError {6    Parse(ParseIntError),7    OutOfRange(i32),8    Io(std::io::Error),9}1011// Display is needed for human-readable error messages12impl fmt::Display for AppError {

The From Trait for Error Conversion

When you implement From<OtherError> for your error type, the ? operator automatically converts OtherError into yours. This creates a unified error type for an entire function.

from_trait.rs
1use std::num::ParseIntError;2use std::io;34#[derive(Debug)]5enum AppError {6    Parse(ParseIntError),7    Io(io::Error),8}910// Implement From so ? can auto-convert11impl From<ParseIntError> for AppError {12    fn from(e: ParseIntError) -> Self {

anyhow and thiserror Crates Overview

These two crates complement each other and cover most error-handling needs in real application and library code.

thiserror — for Library Code

thiserror_example.rs
1// Cargo.toml: thiserror = "1"2use thiserror::Error;34#[derive(Error, Debug)]5enum DataError {6    #[error("failed to read file: {0}")]7    Io(#[from] std::io::Error),89    #[error("invalid format: expected integer, got '{input}'")]10    Parse {11        input: String,12        #[source]13        source: std::num::ParseIntError,14    },1516    #[error("value {0} is out of range 0-100")]17    OutOfRange(i32),18}1920// thiserror generates Display and Error impls automatically

anyhow — for Application Code

anyhow_example.rs
1// Cargo.toml: anyhow = "1"2use anyhow::{Context, Result, bail, ensure};34fn load_config(path: &str) -> Result<String> {5    // anyhow::Result<T> = Result<T, anyhow::Error>6    let content = std::fs::read_to_string(path)7        .with_context(|| format!("failed to open config at '{path}'"))?;89    ensure!(!content.is_empty(), "config file is empty");1011    if content.contains("INVALID") {12        bail!("config contains invalid keyword");
  • Use thiserror to define explicit error types in libraries (callers can pattern-match)
  • Use anyhow for flexible error handling in application code (faster development)

Error Handling Best Practices

  • Return Result instead of panicking for any recoverable error
  • Use the ? operator to keep code clean and readable
  • Implement Display and std::error::Error for custom error types
  • Provide context when propagating errors (e.g., anyhow's with_context)
  • Pattern-match on specific error variants rather than catching everything at once
  • Document the errors your public functions return in their doc comments
Tip
Rust's philosophy: make errors visible in type signatures. If a function can fail, its return type should say so.

Key Takeaways

Điểm Chính

  • Result<T, E> represents success (Ok) or failure (Err)Result<T, E> biểu diễn thành công (Ok) hoặc thất bại (Err)
  • The ? operator propagates errors concisely up the call stackToán tử ? truyền lỗi ngắn gọn lên ngăn xếp gọi
  • Use expect() for unrecoverable errors with a descriptive messageDùng expect() cho lỗi không thể phục hồi với thông điệp mô tả
  • Custom error types implement the std::error::Error traitKiểu lỗi tùy chỉnh triển khai trait std::error::Error

Practice

Test your understanding of this chapter

Quiz

Which variant of Result<T, E> indicates a successful operation?

Biến thể nào của Result<T, E> cho biết thao tác thành công?

Quiz

What does the ? operator do when it encounters an Err(e) value?

Toán tử ? làm gì khi gặp giá trị Err(e)?

True or False

The expect() method is preferable to unwrap() because it lets you provide a descriptive panic message.

Phương thức expect() được ưu tiên hơn unwrap() vì nó cho phép bạn cung cấp thông báo panic mô tả rõ ràng hơn.

True or False

The ? operator can be used inside functions that return the unit type () with no Result or Option in their signature.

Toán tử ? có thể được dùng bên trong các hàm trả về kiểu đơn vị () mà không có Result hay Option trong chữ ký của chúng.

Code Challenge

Propagate an error with the ? operator

Truyền lỗi bằng toán tử ?

fn read_name(path: &str) -> Result<String, io::Error> {
    let mut file = fs::File::open(path);
    let mut s = String::new();
    file.read_to_string(&mut s);
    Ok(s)
}

Chapter Complete!

Great job! Keep the momentum going.

Your progress0 of 19 chapters read
← → to navigate chapters
Built: 4/8/2026, 12:01:11 PM