Xử lý lỗi trong Rust: Result, Option và toán tử ?
Đi sâu vào xử lý lỗi trong Rust: panic không thể phục hồi, Result<T, E> phục hồi được, toán tử lan truyền ?, tạo kiểu lỗi tùy chỉnh và best practices.
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:
- 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. - 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). - 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<T, E>"]
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ểm | Lỗ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ình | File không tồn tại, parse chuỗi thất bại, timeout mạng | Truy 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ử ?, combinators | Tiế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ụng | Lỗ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:
- Rust in thông điệp thông báo lỗi ra luồng
stderr. - 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.
- Tiến trình thoát với mã lỗi khác 0.
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=1trong 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),
}rustKiểm Tra Result Bằng Khối match#
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let greeting_file_result = File::open("hello.txt");
let greeting_file = match greeting_file_result {
Ok(file) => file,
Err(error) => match error.kind() {
ErrorKind::NotFound => match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => panic!("Gặp sự cố khi tạo file: {e:?}"),
},
other_error => {
panic!("Gặp sự cố khi mở file: {other_error:?}");
}
},
};
println!("Mở file thành công: {greeting_file:?}");
}rustCác Phương Thức Rút Gọn: unwrap và expect#
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:
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ế#
use std::fs::File;
use std::io::{self, Read};
// Cách dài dòng dùng match
fn read_username_verbose() -> Result<String, io::Error> {
let username_file_result = File::open("username.txt");
let mut username_file = match username_file_result {
Ok(file) => file,
Err(e) => return Err(e),
};
let mut username = String::new();
match username_file.read_to_string(&mut username) {
Ok(_) => Ok(username),
Err(e) => Err(e),
}
}
// Cách gọn gàng dùng toán tử ?
fn read_username_concise() -> Result<String, io::Error> {
let mut username = String::new();
File::open("username.txt")?.read_to_string(&mut username)?;
Ok(username)
}rustCá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<T, E1>"] --> 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:
use std::fmt;
use std::io;
use std::num::ParseIntError;
#[derive(Debug)]
pub enum AppError {
Io(io::Error),
Parse(ParseIntError),
NotFound(String),
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AppError::Io(err) => write!(f, "Lỗi I/O: {err}"),
AppError::Parse(err) => write!(f, "Lỗi Parse dữ liệu: {err}"),
AppError::NotFound(item) => write!(f, "Không tìm thấy tài nguyên: {item}"),
}
}
}
impl std::error::Error for AppError {}
// Cài đặt chuyển đổi tự động cho toán tử ?
impl From<io::Error> for AppError {
fn from(err: io::Error) -> Self {
AppError::Io(err)
}
}
impl From<ParseIntError> for AppError {
fn from(err: ParseIntError) -> Self {
AppError::Parse(err)
}
}
fn process_port_config() -> Result<u16, AppError> {
let mut content = String::new();
std::fs::File::open("port.txt")?.read_to_string(&mut content)?; // Tự động đổi io::Error -> AppError
let port: u16 = content.trim().parse()?; // Tự động đổi ParseIntError -> AppError
Ok(port)
}rustCác Hàm Chuyển Đổi Hàm (Combinators) Cho Result Và Option#
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àmflên giá trịOk(v), giữ nguyênErr..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ặpErr..unwrap_or_else(f): Tính toán giá trị mặc định từ closure một cách lazy.
fn get_port_or_default() -> u16 {
std::env::var("PORT")
.ok()
.and_then(|p| p.parse::<u16>().ok())
.unwrap_or(8080)
}rustTổ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.