Error Handling trong Rust — Result, Option và câu chuyện `?`

Phong Hy

Hồi mới chuyển từ Go qua Rust, cái đầu tiên mình ức chế nhất là xử lý lỗi. Trong Go quen tay if err != nil rồi, qua Rust tự nhiên compiler nó la um sùm vì mình quên xử lý cái Result với Option. Nhưng càng dùng càng thấy, cái cách Rust xử lý lỗi nó hay thiệt — chặt chẽ mà vẫn gọn gàng, không rườm rà như exception trong mấy ngôn ngữ khác.

Rust programming code on screen Ảnh: Daniil Komov — Pexels

Result vs Option — hai thái cực

Cơ bản nhất, Rust có hai enum để xử lý "không có kết quả":

  • Option<T> — hoặc có giá trị (Some), hoặc không (None)
  • Result<T, E> — hoặc thành công (Ok), hoặc thất bại (Err)
fn find_user(id: u32) -> Option<User> {
    // trả về None nếu không tìm thấy
}

fn divide(a: f64, b: f64) -> Result<f64, String> {
    if b == 0.0 {
        Err("Cannot divide by zero".into())
    } else {
        Ok(a / b)
    }
}

Cái hay là compiler bắt mình xử lý cả hai trường hợp — không có chuyện "quên check null" như trong Java hay TypeScript. Nếu cố tình ignore, Rust sẽ warning hoặc error ngay tại compile time.

? operator — code gọn hơn xíu

Hồi mới tập tọe Rust, mình viết kiểu này hoài:

fn read_config() -> Result<String, io::Error> {
    let file = match fs::read_to_string("config.toml") {
        Ok(f) => f,
        Err(e) => return Err(e),
    };
    Ok(file)
}

Rồi biết tới ? operator — nó làm gọn hơn nhiều:

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

? về cơ bản là: nếu Ok thì unwrap, nếu Err thì return luôn cái lỗi đó. Nhưng có một cái bẫy: ? chỉ dùng được trong function trả về Result hoặc Option. Không dùng trong main() mặc định được — trừ khi mình xài anyhow hoặc Box<dyn Error>.

Custom Error Types với thiserror

Khi project lớn dần, mỗi function trả về một kiểu lỗi khác nhau — io::Error, serde_json::Error, reqwest::Error. Làm sao gom hết lại?

Hồi xưa mình define enum rồi implement From thủ công:

#[derive(Debug)]
enum AppError {
    IoError(io::Error),
    ParseError(serde_json::Error),
}
impl From<io::Error> for AppError { ... }
impl From<serde_json::Error> for AppError { ... }

Viết mấy cái From hoài mệt nghỉ. Có crate thiserror giúp generate tự động:

use thiserror::Error;

#[derive(Error, Debug)]
enum AppError {
    #[error("IO error: {0}")]
    IoError(#[from] io::Error),

    #[error("Failed to parse config")]
    ParseError(#[from] serde_json::Error),
}

Từ đó chỉ cần xài ? trong mọi function — thiserror tự động convert lỗi qua AppError nhờ #[from].

Developer working on code review Ảnh: Godfrey Atima — Pexels

anyhow vs thiserror — khi nào xài cái nào?

Đây là câu hỏi mình gặp nhiều nhất từ mấy bạn mới học Rust. Câu trả lời ngắn gọn:

  • thiserror — dùng khi viết library. Bạn muốn define error types rõ ràng cho consumer xử lý.
  • anyhow — dùng trong application code. Bạn chỉ cần biết "có lỗi", không cần xử lý từng loại cụ thể.

Trong backend service, mình thường dùng anyhow với Context trait:

use anyhow::{Result, Context};

fn process_order(order_id: u32) -> Result<Order> {
    let user = find_user(order_id)
        .context("Failed to find user for order")?;
    let payment = process_payment(user.id)
        .context("Payment failed")?;
    Ok(Order { user, payment })
}

Lỗi trả về có kèm context — đọc log là biết ngay chuyện gì xảy ra, ở đâu, tại sao.

Unwrap, Expect — đừng lạm dụng

Mình thấy nhiều bạn mới hay xài .unwrap() lung tung:

let data = fetch_api().unwrap(); // crash cả app nếu lỗi

Chỉ nên dùng unwrap() hoặc expect() khi mình chắc chắn không thể fail: ví dụ hardcoded config, test code, hoặc fail-fast ở boundary (đọc config khi start app).

let config = Config::from_file("config.toml")
    .expect("Config file must exist at startup");

Còn lại trong production logic, xài ? hoặc match cho mọi thứ.

Mấy cái rút ra sau thời gian code Rust

  1. Đừng lạm dụng Box<dyn Error> — mất thông tin kiểu lỗi, khó xử lý theo từng loại
  2. Dùng thiserror cho library, anyhow cho app — đây gần như convention của cộng đồng
  3. Luôn thêm contextanyhow::Context giúp biết lỗi xảy ra ở bước nào, không chỉ "file not found"
  4. Ghi cả error chain vào log — Sentry với OpenTelemetry support stack trace đầy đủ, debug cực kỳ dễ

Tóm lại

Error handling trong Rust ban đầu hơi choáng, nhưng dùng quen rồi thấy rất đàng hoàng. Không có chuyện quên check error như exception, cũng không phải viết if err != nil như Go. Compiler canh cho hết — mình chỉ cần focus vào logic nghiệp vụ.

Có bạn nào cũng đang học Rust không? Cảm thấy error handling Rust thế nào? Cmt dưới bài nha mình cùng bàn.

📋 Phụ lục thuật ngữ

  • Result<T, E> — enum trong Rust, đại diện cho kết quả có thể thành công (Ok) hoặc thất bại (Err)
  • Option<T> — enum đại diện cho giá trị có thể có (Some) hoặc không (None)
  • ? operator — toán tử rút gọn, unwrap nếu Ok, return Err nếu lỗi
  • thiserror — derive macro để define custom error types tự động
  • anyhow — crate xử lý lỗi cho application code, có Context trait hỗ trợ thêm message
  • Error chain — chuỗi lỗi wrap lồng nhau, mỗi lớp thêm context