Xử Lý Lỗi trong Rust 2026: Result, Option, thiserror và anyhow

Hướng dẫn chi tiết về xử lý lỗi trong Rust với Result, Option, toán tử ?, thiserror và anyhow. Các best practice cho developer Việt Nam.

Xử Lý Lỗi trong Rust 2026: Result, Option, thiserror và anyhow

Xử lý lỗi trong Rust tập trung vào hai enum—Result<T, E>Option<T>—buộc việc xử lý rõ ràng thành công, thất bại và giá trị vắng mặt tại thời điểm biên dịch. Khác với exception lan truyền ngầm, cách tiếp cận của Rust làm cho các đường dẫn lỗi hiện rõ trong signature hàm, loại bỏ toàn bộ các lớp lỗi runtime bất ngờ. Toán tử ?, kết hợp với các crate như thiserroranyhow, đơn giản hóa mô hình rõ ràng này mà không hy sinh tính minh bạch.

Khi Nào Sử Dụng Cái Nào

Sử dụng Option<T> cho các giá trị có thể hợp lệ vắng mặt (trường cấu hình, kết quả tìm kiếm). Sử dụng Result<T, E> khi các thao tác có thể thất bại với thông tin lỗi có ý nghĩa (I/O file, request mạng, parsing).

Hiểu Cơ Bản về Result và Option

Result<T, E> đại diện cho thành công (Ok(T)) hoặc thất bại (Err(E)). Option<T> đại diện cho có giá trị (Some(T)) hoặc vắng mặt (None). Cả hai đều là sum types—compiler đảm bảo mọi variant đều được xử lý.

error_examples.rsrust
// Demonstrates Result and Option basic patterns

fn find_user(id: u64) -> Option<String> {
    // Returns None if user doesn't exist
    if id == 0 {
        None
    } else {
        Some(format!("User-{}", id))
    }
}

fn parse_port(s: &str) -> Result<u16, std::num::ParseIntError> {
    // Returns Err if parsing fails
    s.parse::<u16>()
}

fn main() {
    // Option handling - must address None case
    match find_user(42) {
        Some(name) => println!("Found: {}", name),
        None => println!("User not found"),
    }

    // Result handling - must address Err case
    match parse_port("8080") {
        Ok(port) => println!("Port: {}", port),
        Err(e) => println!("Invalid port: {}", e),
    }
}

Compiler từ chối code bỏ qua các giá trị return này mà không xử lý rõ ràng. Thiết kế này bắt lỗi tại thời điểm biên dịch mà sẽ xuất hiện dưới dạng null pointer exception hoặc uncaught error trong các ngôn ngữ khác.

Toán Tử Dấu Hỏi Cho Việc Lan Truyền Súc Tích

Toán tử ? biến đổi các chuỗi match dài dòng thành code tuyến tính dễ đọc. Khi áp dụng cho Result, nó return early với error nếu có, hoặc unwrap giá trị thành công. Tương tự áp dụng cho Option.

file_reader.rsrust
// Using ? for clean error propagation

use std::fs::File;
use std::io::{self, BufRead, BufReader};

fn read_first_line(path: &str) -> Result<String, io::Error> {
    let file = File::open(path)?;  // Returns early if open fails
    let mut reader = BufReader::new(file);
    let mut line = String::new();
    reader.read_line(&mut line)?;  // Returns early if read fails
    Ok(line.trim().to_string())
}

fn get_port_from_config(path: &str) -> Result<u16, Box<dyn std::error::Error>> {
    let content = read_first_line(path)?;
    let port = content.parse::<u16>()?;  // ParseIntError converts via From
    Ok(port)
}

Toán tử ? yêu cầu kiểu error có thể chuyển đổi sang kiểu error return của hàm thông qua trait From. Sử dụng Box<dyn std::error::Error> như trên chấp nhận bất kỳ kiểu error nào implement trait Error tiêu chuẩn.

Tạo Custom Error với thiserror

Crate thiserror loại bỏ boilerplate cho các kiểu error tùy chỉnh. Nó tạo ra các implementation Error, Display, và From thông qua procedural macro.

errors.rsrust
// Custom error types using thiserror 2.0

use thiserror::Error;

#[derive(Error, Debug)]
pub enum ConfigError {
    #[error("configuration file not found at {path}")]
    NotFound { path: String },

    #[error("invalid port number: {0}")]
    InvalidPort(#[from] std::num::ParseIntError),

    #[error("IO error reading config")]
    IoError(#[from] std::io::Error),

    #[error("missing required field: {0}")]
    MissingField(String),
}

fn load_config(path: &str) -> Result<Config, ConfigError> {
    let content = std::fs::read_to_string(path)?;  // IoError auto-converts
    let port: u16 = content
        .lines()
        .find(|l| l.starts_with("port="))
        .ok_or(ConfigError::MissingField("port".into()))?
        .strip_prefix("port=")
        .unwrap()
        .parse()?;  // ParseIntError auto-converts to InvalidPort

    Ok(Config { port })
}

struct Config {
    port: u16,
}

Attribute #[from] tạo ra các chuyển đổi tự động, cho phép sử dụng ? liền mạch với các kiểu error cơ bản khác nhau. Thông điệp error trở nên tự mô tả thông qua format string #[error(...)].

Error Cấp Ứng Dụng với anyhow

Trong khi thiserror phù hợp cho code library với các kiểu error cụ thể, anyhow nhắm đến ứng dụng nơi ngữ cảnh error quan trọng hơn độ chi tiết kiểu. Trait Context của nó thêm các thông điệp mô tả vào bất kỳ error nào.

main.rsrust
// Application error handling with anyhow 1.0

use anyhow::{Context, Result, bail, ensure};

fn load_database_url() -> Result<String> {
    std::env::var("DATABASE_URL")
        .context("DATABASE_URL environment variable not set")
}

fn connect_to_database(url: &str) -> Result<DatabaseConnection> {
    ensure!(!url.is_empty(), "database URL cannot be empty");

    let conn = DatabaseConnection::new(url)
        .context("failed to establish database connection")?;

    if !conn.is_healthy() {
        bail!("database connection unhealthy after establishment");
    }

    Ok(conn)
}

fn main() -> Result<()> {
    let url = load_database_url()?;
    let conn = connect_to_database(&url)
        .context("application startup failed")?;

    // Context chains create readable error traces:
    // Error: application startup failed
    // Caused by:
    //     0: failed to establish database connection
    //     1: connection refused

    Ok(())
}

struct DatabaseConnection;
impl DatabaseConnection {
    fn new(_url: &str) -> Result<Self> { Ok(Self) }
    fn is_healthy(&self) -> bool { true }
}

Method context() bọc error với thông tin bổ sung, tạo ra một chuỗi hỗ trợ debugging. Macro bail! cung cấp early exit với thông điệp error được format, trong khi ensure! hoạt động như assertion trả về error thay vì panic.

Sẵn sàng chinh phục phỏng vấn Rust?

Luyện tập với mô phỏng tương tác, flashcards và bài kiểm tra kỹ thuật.

Kết Hợp thiserror và anyhow trong Dự Án Thực Tế

Library phơi bày error có cấu trúc qua thiserror để xử lý programmatic bởi người dùng. Ứng dụng bọc các error đó với anyhow cho output dễ đọc với con người. Sự phân tách này giữ API sạch sẽ trong khi duy trì khả năng debug.

lib.rs - Library code with thiserrorrust
// Exposes typed errors for programmatic handling

use thiserror::Error;

#[derive(Error, Debug)]
pub enum PaymentError {
    #[error("insufficient funds: required {required}, available {available}")]
    InsufficientFunds { required: u64, available: u64 },

    #[error("card declined: {reason}")]
    CardDeclined { reason: String },

    #[error("payment provider unavailable")]
    ProviderUnavailable(#[source] reqwest::Error),
}

pub fn process_payment(amount: u64) -> Result<Receipt, PaymentError> {
    // Library returns specific, matchable error types
    Err(PaymentError::InsufficientFunds {
        required: amount,
        available: 50,
    })
}

pub struct Receipt;
main.rs - Application code with anyhowrust
// Wraps library errors with context

use anyhow::{Context, Result};
use my_payment_lib::{process_payment, PaymentError};

fn checkout(cart_total: u64) -> Result<()> {
    match process_payment(cart_total) {
        Ok(_receipt) => Ok(()),
        Err(PaymentError::InsufficientFunds { required, available }) => {
            // Handle specific case differently
            println!("Add {} to your balance", required - available);
            Ok(())
        }
        Err(e) => Err(e).context("checkout payment processing failed"),
    }
}

Pattern này cho phép caller match trên các variant cụ thể khi recovery khả thi, trong khi vẫn được hưởng lợi từ ngữ cảnh error phong phú khi propagating failure lên trên. Cộng đồng Rust phần lớn đã chuẩn hóa cách tiếp cận này, như được thảo luận trong hướng dẫn API Rust.

Các Pattern Xử Lý Error cho Code Async

Các hàm async trả về Result giống như synchronous. Toán tử ? hoạt động giống hệt trong các khối async, và cả thiserror lẫn anyhow tích hợp mà không cần sửa đổi.

async_errors.rsrust
// Error handling in async Rust with Tokio

use anyhow::{Context, Result};
use std::time::Duration;

async fn fetch_user_data(user_id: u64) -> Result<UserData> {
    let response = reqwest::get(format!("https://api.example.com/users/{}", user_id))
        .await
        .context("HTTP request to user API failed")?;

    let status = response.status();
    if !status.is_success() {
        anyhow::bail!("user API returned status {}", status);
    }

    let data: UserData = response
        .json()
        .await
        .context("failed to parse user data JSON")?;

    Ok(data)
}

async fn fetch_with_retry(user_id: u64, attempts: u32) -> Result<UserData> {
    let mut last_error = None;

    for attempt in 1..=attempts {
        match fetch_user_data(user_id).await {
            Ok(data) => return Ok(data),
            Err(e) => {
                last_error = Some(e);
                if attempt < attempts {
                    tokio::time::sleep(Duration::from_millis(100 * attempt as u64)).await;
                }
            }
        }
    }

    Err(last_error.unwrap()).context(format!("failed after {} attempts", attempts))
}

#[derive(serde::Deserialize)]
struct UserData {
    name: String,
}

Khi kết hợp nhiều thao tác async, sử dụng try_join! từ tokio hoặc futures để chạy chúng đồng thời trong khi propagating error đầu tiên. Để tìm hiểu các khái niệm liên quan, khám phá module câu hỏi phỏng vấn async/await.

Downcasting và Kiểm Tra Error

Cả anyhow::ErrorBox<dyn Error> đều hỗ trợ downcasting để khôi phục kiểu error gốc. Điều này cho phép logging chi tiết cụ thể trong khi vẫn propagating error chung.

downcasting.rsrust
// Inspecting wrapped error types

use anyhow::{Context, Result};
use std::io;

fn log_and_propagate(result: Result<()>) -> Result<()> {
    if let Err(ref e) = result {
        // Check if the root cause is a specific type
        if let Some(io_err) = e.downcast_ref::<io::Error>() {
            match io_err.kind() {
                io::ErrorKind::NotFound => {
                    tracing::warn!("file not found, using defaults");
                }
                io::ErrorKind::PermissionDenied => {
                    tracing::error!("permission denied - check file ownership");
                }
                _ => {
                    tracing::error!("IO error: {:?}", io_err);
                }
            }
        }
    }
    result
}

Downcasting bắc cầu khoảng cách giữa xử lý error chung và logic recovery cụ thể. Sử dụng thận trọng—nếu downcasting xảy ra thường xuyên, hãy cân nhắc liệu enum error có kiểu sẽ phục vụ tốt hơn.

Chuyển Đổi Giữa Option và Result

Thư viện chuẩn cung cấp các method để chuyển đổi giữa OptionResult, cho phép composition mượt mà khi các API khác nhau sử dụng các pattern khác nhau.

conversions.rsrust
// Option and Result interoperability

fn get_env_port() -> Option<u16> {
    std::env::var("PORT")
        .ok()  // Result -> Option (discards error)
        .and_then(|s| s.parse().ok())
}

fn get_env_port_with_error() -> Result<u16, String> {
    std::env::var("PORT")
        .map_err(|_| "PORT not set".to_string())?
        .parse()
        .map_err(|_| "PORT is not a valid number".to_string())
}

fn lookup_and_parse(map: &std::collections::HashMap<String, String>, key: &str) -> Result<u16, String> {
    map.get(key)
        .ok_or_else(|| format!("key '{}' not found", key))?  // Option -> Result
        .parse()
        .map_err(|e| format!("parse error for '{}': {}", key, e))
}

Method ok() loại bỏ chi tiết error khi chỉ sự hiện diện mới quan trọng. Các method ok_or()ok_or_else() chuyển đổi None thành custom error, cho phép propagation ? từ các giá trị Option. Các pattern này thường xuất hiện trong câu hỏi phỏng vấn pattern matching.

Cân Nhắc Về Hiệu Năng

Xử lý error trong Rust không mang chi phí runtime trên đường dẫn thành công. ResultOption là enum được cấp phát trên stack với kích thước xác định. Compiler tối ưu hóa các kiểm tra khi có thể chứng minh một nhánh không thể đạt tới.

| Cách tiếp cận | Chi phí Đường dẫn Thành công | Chi phí Đường dẫn Thất bại | |---------------|------------------------------|----------------------------| | Result/Option | Không | Stack unwinding (rẻ) | | panic! | Không | Full stack unwinding + cleanup | | Exception C++ | Không (thường) | Cấp phát heap đắt + RTTI |

Tránh unwrap()expect() trong code library—dành cho các trường hợp thất bại thực sự chỉ ra bug. Với các đường dẫn quan trọng về hiệu năng nơi error phổ biến, cân nhắc sử dụng enum với dữ liệu inline thay vì các kiểu error được cấp phát trên heap.

Kết Luận

  • Result<T, E> xử lý các thất bại có thể khôi phục; Option<T> xử lý sự vắng mặt—cả hai buộc xử lý tại thời điểm biên dịch
  • Toán tử ? propagate error súc tích, yêu cầu implementation trait From cho chuyển đổi kiểu
  • thiserror tạo các kiểu error có cấu trúc cho library mà không cần boilerplate
  • anyhow cung cấp các chuỗi error theo ngữ cảnh cho ứng dụng, hỗ trợ context(), bail!, và ensure!
  • Kết hợp cả hai: library phơi bày error có kiểu qua thiserror, ứng dụng bọc chúng với anyhow
  • Code async sử dụng các pattern giống hệt—? hoạt động trong các khối async mà không cần sửa đổi
  • Sử dụng ok_or() để chuyển đổi Option sang Result; sử dụng ok() cho ngược lại
  • Downcast wrapped error chỉ khi logic recovery cụ thể yêu cầu kiểu gốc

Bắt đầu luyện tập!

Kiểm tra kiến thức với mô phỏng phỏng vấn và bài kiểm tra kỹ thuật.

Thẻ

#rust
#error handling
#thiserror
#anyhow

Chia sẻ

Bài viết liên quan