blog.dopana

Back

Xử lý lỗi trong Rust sở hữu triết lý rất khác biệt so với các ngôn ngữ phổ biến khác. Rust không dùng các khối ngoại lệ try/catch và hoàn toàn không có con trỏ null. Thay vào đó, ngôn ngữ này biến việc xử lý lỗi trở nên tường minh, an toàn kiểu dữ liệu (type-safe) và được bộ biên dịch thẩm định chặt chẽ ngay tại thời điểm biên dịch.

Explain Like I’m 10: Dịch Vụ Giao Hàng Bưu Điện#

Hãy hình dung việc gọi một hàm trong Rust giống như gửi một món bưu kiện qua bưu điện:

  1. Giao hàng thành công (Ok(GóiHàng)): Bác bưu tá bấm chuông và trao tận tay bạn món đồ nguyên vẹn.
  2. Sự cố có thể xử lý (Err(LýDo)): Địa chỉ ghi thiếu số nhà hoặc người nhận vắng mặt. Bưu điện sẽ gửi thông báo lại cho người gửi để quyết định phương án tiếp theo (gửi lại, gọi điện xác nhận hoặc chuyển hướng).
  3. Thảm họa nghiêm trọng (panic!): Xe tải chở hàng gặp tai nạn nổ máy hoàn toàn hoặc bưu cục bị sập. Mọi hoạt động buộc phải dừng ngay lập tức vì không còn cách nào an toàn để tiếp tục.
graph TD
    Op["Thao tác / Lệnh gọi hàm"] --> Choice{"Lỗi có thể phục hồi được không?"}
    Choice -->|Không: Bug nghiêm trọng / Vi phạm bất biến| Panic["Macro panic!<br/>(Dừng chương trình, Unwind Stack, Thoát)"]
    Choice -->|Có: Không tìm thấy file, Lỗi mạng, Dữ liệu sai| ResultEnum["Result&lt;T, E&gt;"]
    ResultEnum --> OkBranch["Ok(giá trị) -> Thành công"]
    ResultEnum --> ErrBranch["Err(lỗi) -> Xử lý hoặc Lan truyền bằng ?"]

Hai Nhóm Lỗi Trong Rust#

Đặc điểmLỗi có thể phục hồi (Result<T, E>)Lỗi không thể phục hồi (panic!)
Cơ chếEnum Result<T, E> (Ok hoặc Err)Macro panic!("...")
Nguyên nhân điển hìnhFile không tồn tại, parse chuỗi thất bại, timeout mạngTruy cập mảng vượt chỉ số, hỏng bất biến bộ nhớ
Cách xử lýmatch, if let, toán tử ?, combinatorsTiến trình dừng hoạt động hoặc tháo dỡ stack
Triết lý thiết kếMột phần dự kiến trước của luồng ứng dụngLỗi lập trình nghiêm trọng; không thể tiếp tục an toàn

Lỗi Không Thể Phục Hồi Với panic!#

Khi macro panic! được kích hoạt:

  1. Rust in thông điệp thông báo lỗi ra luồng stderr.
  2. Theo mặc định, chương trình thực hiện quá trình unwind stack — duyệt ngược ngăn xếp để giải phóng bộ nhớ và dọn dẹp tài nguyên.
  3. Tiến trình thoát với mã lỗi khác 0.
src/main.rs
fn main() {
    // Gọi panic chủ động
    // panic!("Lỗi hệ thống: Connection pool cơ sở dữ liệu bị hỏng");

    // Lỗi runtime tự động gây panic
    let numbers = vec![10, 20, 30];
    let _item = numbers[99]; // panics: index out of bounds: the len is 3 but the index is 99
}
rust

[!NOTE] Thiết lập biến môi trường RUST_BACKTRACE=1 trong terminal sẽ hiển thị toàn bộ ngăn xếp hàm (call stack) giúp xác định chính xác dòng code bắt đầu xảy ra panic.

Lỗi Có Thể Phục Hồi Với Result<T, E>#

Rust định nghĩa sẵn Result trong thư viện chuẩn:

enum Result<T, E> {
    Ok(T),
    Err(E),
}
rust

Kiểm Tra Result Bằng Khối match#

Các Phương Thức Rút Gọn: unwrapexpect#

Khi viết nhanh prototype hoặc viết kiểm thử unit test, Result cung cấp các hàm lấy dữ liệu thuận tiện:

src/main.rs
use std::fs::File;

// 1. .unwrap() -> trả về giá trị nếu Ok, panic với thông báo mặc định nếu Err
let f1 = File::open("config.json").unwrap();

// 2. .expect(msg) -> trả về giá trị nếu Ok, panic kèm thông điệp cụ thể nếu Err
let f2 = File::open("config.json").expect("config.json là file bắt buộc để khởi động ứng dụng");
rust

[!TIP] Trong môi trường production, hãy luôn ưu tiên sử dụng .expect() thay vì .unwrap(). Thông điệp lỗi chi tiết sẽ giúp tiết kiệm rất nhiều thời gian khi kiểm tra log hệ thống.

Lan Truyền Lỗi Bằng Toán Tử ?#

Thay vì phải viết khối match lặp đi lặp lại ở mọi nơi, toán tử ? tự động giải nén giá trị Ok(T) hoặc trả về Err(E) ngay lập tức thoát khỏi hàm hiện tại:

sequenceDiagram
    participant Caller as Ham Goi Caller
    participant Worker as read_username
    participant FS as He Thong File

    Worker->>FS: File::open("username.txt")
    alt File ton tai
        FS-->>Worker: Ok(file)
        Worker->>FS: read_to_string()
        alt Doc file thanh cong
            FS-->>Worker: Ok(bytes)
            Worker-->>Caller: Ok(username)
        else Doc file that bai
            FS-->>Worker: Err(io_error)
            Worker-->>Caller: Tra ve Err(io_error) qua ?
        end
    else Khong tim thay file
        FS-->>Worker: Err(io_error)
        Worker-->>Caller: Tra ve Err(io_error) qua ?
    end

So Sánh Cú Pháp Thực Tế#

Cách Toán Tử ? Chuyển Đổi Kiểu Tự Động#

Toán tử ? ngầm thực hiện lệnh gọi From::from lên kiểu lỗi, tự động chuyển đổi lỗi cụ thể thành kiểu lỗi trả về của hàm:

graph LR
    Expr["Biểu thức trả về Result&lt;T, E1&gt;"] --> Check{"Kiểm tra Ok hay Err?"}
    Check -->|Ok value| Extract["Lấy giá trị value và tiếp tục thực thi"]
    Check -->|Err e1| Convert["Chuyển đổi e1 qua From::from thành E2"]
    Convert --> ReturnEarly["Lập tức return Err(e2) ra ngoài hàm"]

Xây Dựng Kiểu Lỗi Tùy Chỉnh (Custom Error Types)#

Trong các ứng dụng thực tế, một hàm có thể gặp nhiều loại lỗi khác nhau (lỗi I/O, lỗi parse định dạng JSON, lỗi cơ sở dữ liệu). Chúng ta thường tổng hợp chúng vào một Enum duy nhất:

Các Hàm Chuyển Đổi Hàm (Combinators) Cho ResultOption#

Rust cung cấp các phương thức phong cách functional giúp biến đổi lỗi linh hoạt:

  • .map(f): Áp dụng hàm f lên giá trị Ok(v), giữ nguyên Err.
  • .and_then(f): Nối tiếp một thao tác khác có khả năng trả về Result.
  • .unwrap_or(default): Lấy giá trị bên trong hoặc trả về giá trị mặc định fallback nếu gặp Err.
  • .unwrap_or_else(f): Tính toán giá trị mặc định từ closure một cách lazy.
src/main.rs
fn get_port_or_default() -> u16 {
    std::env::var("PORT")
        .ok()
        .and_then(|p| p.parse::<u16>().ok())
        .unwrap_or(8080)
}
rust

Tổng kết#

  • Sử dụng panic! cho các lỗi lập trình không thể khắc phục và vi phạm tính đúng đắn của ứng dụng.
  • Sử dụng Result<T, E> cho các trường hợp dự đoán được lỗi để nơi gọi hàm chủ động xử lý.
  • Toán tử ? giúp viết mã nguồn sạch sẽ, không tốn chi phí phụ trội runtime (zero-cost) và tự động ép kiểu lỗi.
  • Thiết kế custom Enum errors giúp quản lý tập trung và nhất quán toàn bộ các kịch bản lỗi trong dự án.

Tài liệu tham khảo#