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 tập trung vào hai enum—Result<T, E> và 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ư thiserror và anyhow, đơn giản hóa mô hình rõ ràng này mà không hy sinh tính minh bạch.
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ý.
// 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.
// 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.
// 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.
// 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.
// 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;// 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.
// 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::Error và Box<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.
// 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 Option và Result, cho phép composition mượt mà khi các API khác nhau sử dụng các pattern khác nhau.
// 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() và 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. Result và Option 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() và 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 traitFromcho chuyển đổi kiểu thiserrortạo các kiểu error có cấu trúc cho library mà không cần boilerplateanyhowcung 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ớianyhow - 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 đổiOptionsangResult; sử dụngok()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ẻ
Chia sẻ
Bài viết liên quan

Rust Traits và Generics năm 2026: Trait Upcasting, AsyncFn và Câu Hỏi Phỏng Vấn Nâng Cao
Hướng dẫn chuyên sâu về traits và generics trong Rust với các tính năng mới của Edition 2024: trait upcasting, AsyncFn closures, RPITIT và các pattern nâng cao thường gặp trong phỏng vấn kỹ thuật.

Con trỏ thông minh trong Rust: Box, Rc, Arc và RefCell năm 2026
Con trỏ thông minh Box, Rc, Arc và RefCell trong Rust được giải thích qua các ví dụ biên dịch được năm 2026, bảng quyết định và những câu hỏi phỏng vấn thường gặp.

Async/Await trong Rust: Giải thích Tokio, Futures và Đồng thời Bất đồng bộ
Tìm hiểu chi tiết về lập trình bất đồng bộ trong Rust với async/await, runtime Tokio, trait Future, và các mẫu thiết kế đồng thời phổ biến trong phỏng vấn kỹ thuật.