Tình huống
Bạn đang refactor code xử lý file I/O, thêm ? vào lệnh File::open() để bỏ qua .unwrap() dài dòng, và Rust ngay lập tức báo lỗi:
error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`)
--> src/main.rs:3:29
|
3 | let f = File::open("data.txt")?;
| ^ cannot use the `?` operator in a function that returns `()`
|
= help: the trait `FromResidual<Result<Infallible, std::io::Error>>` is not implemented for `()`
Hàm của bạn trả về () — không có gì. Toán tử ? cần nơi để truyền lỗi đi. Không có Result trong phạm vi nghĩa là không có chỗ nào để lỗi được truyền tới.
Tại sao lỗi này xảy ra
Toán tử ? là cú pháp rút gọn cho việc return sớm khi gặp lỗi. Viết some_fn()? tương đương với:
match some_fn() {
Ok(val) => val,
Err(e) => return Err(e.into()), // trả về sớm
}
Lệnh return Err(e) đó yêu cầu hàm bao ngoài phải trả về Result. Nếu không, sẽ không có đường return hợp lệ. Cùng logic áp dụng cho Option: ? trên một Option sẽ return None sớm, nên hàm cũng phải trả về Option.
Cách sửa 1: Thay đổi main() để trả về Result
main() là nơi lỗi này thường gặp nhất. Thêm kiểu trả về và Ok(()) ở cuối:
use std::fs::File;
use std::io::Read;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut f = File::open("data.txt")?;
let mut contents = String::new();
f.read_to_string(&mut contents)?;
println!("{}", contents);
Ok(())
}
Box<dyn std::error::Error> là kiểu bắt-tất-cả — chấp nhận bất kỳ lỗi nào implement trait Error. Đủ dùng cho hầu hết các binary và script nhỏ.
Nếu dùng crate anyhow, code còn gọn hơn:
use anyhow::Result;
fn main() -> Result<()> {
let contents = std::fs::read_to_string("data.txt")?;
println!("{}", contents);
Ok(())
}
Cách sửa 2: Thay đổi kiểu trả về của hàm thông thường
Bất kỳ hàm nào cũng có thể dùng ? — chỉ cần kiểu trả về đúng. Đây là trước và sau:
// Trước — lỗi compiler
fn load_config() {
let contents = std::fs::read_to_string("config.toml")?;
// ...
}
// Sau — biên dịch thành công
fn load_config() -> Result<String, std::io::Error> {
let contents = std::fs::read_to_string("config.toml")?;
Ok(contents)
}
Xử lý nhiều kiểu lỗi trong một hàm? Dùng boxed error giúp bạn tránh phải viết các conversion thủ công giữa chúng:
fn load_and_parse() -> Result<MyConfig, Box<dyn std::error::Error>> {
let contents = std::fs::read_to_string("config.toml")?;
let config: MyConfig = toml::from_str(&contents)?; // kiểu lỗi khác, không vấn đề gì
Ok(config)
}
Cách sửa 3: Dùng ? bên trong closure
Closure phức tạp hơn. Đoạn code này sẽ không biên dịch được:
let results: Vec<_> = paths.iter().map(|p| {
let content = std::fs::read_to_string(p)?; // lỗi ở đây
content
}).collect();
Kiểu trả về được suy luận của closure là String, không phải Result<String, _>. Hãy làm cho closure trả về Result một cách tường minh, sau đó collect vào Result<Vec> — cách này dừng lại ngay khi gặp lỗi đầu tiên:
let results: Result<Vec<_>, _> = paths.iter().map(|p| {
std::fs::read_to_string(p)
}).collect();
match results {
Ok(contents) => { /* đọc tất cả file thành công */ }
Err(e) => eprintln!("Lỗi: {}", e),
}
Muốn bỏ qua lỗi một cách im lặng? Dùng .ok() với filter_map:
let contents: Vec<String> = paths.iter()
.filter_map(|p| std::fs::read_to_string(p).ok())
.collect();
Cách sửa 4: Kết hợp Result và Option
Kết hợp Result và Option trong một hàm là bẫy thường gặp. Chúng là hai kiểu khác nhau — bạn không thể dùng ? trên cả hai mà không có conversion:
use std::collections::HashMap;
// Lỗi — hàm trả về Result nhưng ? được dùng trên Option
fn get_value(map: &HashMap<String, String>, key: &str) -> Result<String, String> {
let val = map.get(key)?; // Option, không phải Result!
Ok(val.clone())
}
Chuyển Option thành Result bằng .ok_or():
fn get_value(map: &HashMap<String, String>, key: &str) -> Result<String, String> {
let val = map.get(key).ok_or_else(|| format!("không tìm thấy key '{}'", key))?;
Ok(val.clone())
}
Chiều ngược lại — hàm trả về Option, nhưng bạn có một Result — dùng .ok() để bỏ qua lỗi:
fn try_read(path: &str) -> Option<String> {
std::fs::read_to_string(path).ok()
}
Kiểm tra sau khi sửa
Trước khi build toàn bộ, chạy kiểm tra kiểu nhanh:
cargo check
Không còn E0277 nghĩa là bạn đã xong. Sau đó chạy test:
cargo test
Đã thay đổi signature của main()? Build toàn bộ để xác nhận không có gì bị lỗi runtime:
cargo run
Tóm tắt nhanh
- Trong main() — đổi thành
fn main() -> Result<(), Box<dyn Error>>, thêmOk(())ở cuối - Trong hàm — đổi kiểu trả về thành
Result<T, E>hoặcOption<T>, return giá trị được wrap trongOk()hoặcSome() - Trong closure — làm cho closure trả về
Result, collect vàoResult<Vec<_>, _> - Kết hợp kiểu — dùng
.ok_or()để chuyểnOption → Result, hoặc.ok()để chuyểnResult → Option
E0277 là một trong những lỗi hữu ích nhất của Rust — nó cho bạn biết chính xác hàm nào cần kiểu trả về khác và trỏ trực tiếp vào ? gây ra vấn đề. Khi các kiểu trả về đã khớp, ? là một trong những pattern truyền lỗi gọn gàng nhất trong các ngôn ngữ lập trình hệ thống.

