Sửa lỗi 'thread main panicked at called `Option::unwrap()` on a `None` value' trong Rust

beginner🦀 Rust2026-04-28| Rust (tất cả phiên bản), Linux/macOS/Windows

Error Message

thread 'main' panicked at 'called `Option::unwrap()` on a `None` value'
#rust#option#unwrap#panic#xử-lý-lỗi#result

Sự Cố

2 giờ sáng. Log production đang la hét. Service Rust của bạn bị mắc kẹt trong vòng lặp crash, khởi động lại mỗi vài giây. Bạn mở stderr lên và thấy:

thread 'main' panicked at 'called `Option::unwrap()` on a `None` value', src/main.rs:42:25

Hoặc người anh em cùng mức độ tàn phá của nó:

thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: "Connection refused"'

Tại một thời điểm nào đó, bạn đã gọi .unwrap() vì bạn chắc chắn rằng giá trị đó sẽ luôn tồn tại. Trên môi trường production, "luôn luôn" là một canh bạc mà sớm muộn bạn cũng thua. Một file config biến mất, một truy vấn database trả về rỗng, một biến môi trường không bao giờ được thiết lập—và ứng dụng chết kèm theo stack trace thay vì một thông báo lỗi gọn gàng.

Tại Sao Điều Này Xảy Ra

Các kiểu Option<T>Result<T, E> trong Rust buộc bạn phải thừa nhận ngay từ đầu rằng có thể có gì đó bị thiếu hoặc bị hỏng. Gọi .unwrap() là một lối tắt bỏ qua sự thừa nhận đó. Nó nói với compiler rằng: "Hãy tin tôi, giá trị này luôn có ở đó—và nếu không có, hãy crash toàn bộ thread."

Ổn thôi với các script dùng một lần hay unit test. Nhưng là một quả mìn trong bất cứ thứ gì đưa đến tay người dùng.

Các Bước Khắc Phục

1. Tìm Nguồn Gốc với Backtrace

Đôi khi panic trỏ đến một dòng sâu bên trong một crate bên thứ ba, không phải code của bạn. Chạy với backtrace được bật để lấy toàn bộ call stack:

RUST_BACKTRACE=1 cargo run

Trên một web service thông thường, đầu ra này có thể chạy sâu tới 30–50 frame. Hãy quét tìm các frame có tham chiếu đến src/—đó là code của bạn. Mọi thứ còn lại là nội bộ của thư viện, bạn có thể bỏ qua chúng cho lúc này.

2. Giải Pháp Nhanh: Chuyển sang expect()

Nếu vị trí panic xảy ra lúc khởi động và crash thực sự là kết quả đúng đắn (ví dụ như thiếu config quan trọng), ít nhất hãy thay unwrap() bằng expect(). Hành vi tương tự, nhưng thông báo lỗi tốt hơn nhiều.

// ❌ Khó hiểu khi thất bại
let config = read_config().unwrap();

// ✅ Cho bạn biết chính xác điều gì đã xảy ra
let config = read_config().expect("Không thể tải config.yaml. File có tồn tại trong thư mục làm việc không?");

Một thông điệp expect được viết tốt sẽ giúp kỹ sư tiếp theo tiết kiệm mười phút phỏng đoán.

3. Xử Lý Tường Minh bằng Pattern Matching

Đối với bất cứ thứ gì cần tồn tại khi giá trị bị thiếu, hãy dùng match hoặc if let. Cả hai đều buộc bạn phải viết code cho trường hợp rỗng—đó chính là điểm mấu chốt.

// match: xử lý cả hai nhánh
let user_name = match get_user(id) {
    Some(user) => user.name,
    None => "Anonymous".to_string(),
};

// if let: chỉ hành động khi giá trị tồn tại
if let Some(user) = get_user(id) {
    println!("Xin chào, {}!", user.name);
} else {
    println!("Không tìm thấy người dùng.");
}

4. Cung Cấp Giá Trị Mặc Định với unwrap_or

Có một fallback hợp lý? Hãy bỏ qua match hoàn toàn.

// Giá trị mặc định tĩnh
let port = std::env::var("PORT").unwrap_or_else(|_| "8080".to_string());

// Giá trị mặc định lazy — closure chỉ chạy khi giá trị bị thiếu
let data = fetch_cached_data().unwrap_or_else(|| fetch_from_api());

Dùng unwrap_or_else (với closure) khi việc tính toán fallback tốn kém—nó sẽ không chạy trừ khi cần thiết.

5. Lan Truyền với Toán Tử ?

Hầu hết thời gian, một hàm gặp phải None hoặc Err không nên tự quyết định phải làm gì với nó—đó là việc của người gọi. Toán tử ? tự động return sớm kèm theo lỗi:

fn get_api_key() -> Result<String, ConfigError> {
    // Nếu var() trả về Err, hàm thoát tại đây và trả về Err đó
    let key = std::env::var("API_KEY")?;
    Ok(key)
}

Xâu chuỗi nhiều cái như vậy lại và happy path của bạn vẫn gọn gàng. Lỗi nổi lên đến tầng nào thực sự được trang bị để xử lý chúng—thường là một handler cấp cao nhất ghi log thất bại và phản hồi với HTTP 500 hoặc exit code thích hợp.

Kiểm Tra

Hãy tái hiện lỗi một cách có chủ đích trước khi tuyên bố đã sửa xong:

- Xóa biến môi trường hoặc xóa file đã kích hoạt `None` hoặc `Err`.
- Chạy ứng dụng.
- Xác nhận rằng ứng dụng hoặc phục hồi một cách graceful (ghi log cảnh báo, sử dụng fallback) hoặc thoát với thông báo lỗi rõ ràng—không panic, không stack trace đổ ra stderr.

Mẹo cho Production

- **Biến unwrap thành lỗi build:** Thêm `#![deny(clippy::unwrap_used)]` vào đầu `main.rs` hoặc `lib.rs`. CI sẽ từ chối bất kỳ PR nào lén mang vào một `unwrap()` trần trụi.
- **anyhow cho app, thiserror cho thư viện:** Crate `anyhow` cho phép bạn bọc bất kỳ kiểu lỗi nào chỉ với một `?` và đính kèm ngữ cảnh bằng `.context("bạn đang làm gì")`. Dùng `thiserror` khi bạn đang publish một crate và người gọi cần match trên các biến thể lỗi cụ thể.
- **Ghi log các lỗi đã được xử lý:** Khi bạn bắt được `None` hoặc `Err` và tiếp tục, hãy ghi log với `tracing::warn!` hoặc `log::warn!`. Các fallback im lặng che giấu vấn đề thực sự—cảnh báo hôm nay là sự cố của ngày mai.

Related Error Notes