Mục lục
Mục Tiêu Bài Học
Sau bài học, bạn sẽ:
- Hiểu macro
panic!làm gì ở runtime: abort thread hiện tại, in error + (tuỳ chọn) backtrace, không cho resume execution. - Phân biệt rõ trường hợp nên dùng
panic!(programmer bug, invariant violation, prototype) và trường hợp nên dùngResult(file not found, network lỗi, input người dùng sai). - Nhận diện các điểm trong stdlib có thể panic ngầm:
v[10]out-of-bound, byte slice cắt giữa multi-byte char, chia cho 0, integer overflow ở debug profile,unwrap()trênNonehoặcErr. - Biết syntax của
panic!: format string giốngprintln!, ví dụpanic!("invalid: {value}"). - Hiểu khác nhau giữa
panic = "unwind"(default — chạy destructor) vàpanic = "abort"(exit ngay, binary nhỏ hơn). - Biết khi nào dùng
std::panic::catch_unwind(FFI boundary, test framework) và khi nào KHÔNG nên (không phải exception generic-purpose). - Bật
RUST_BACKTRACE=1để in stack, hoặcRUST_BACKTRACE=fullđể có full backtrace khi debug.
Đây là bài đầu Nhóm 19. Các bài tiếp theo (141 - 149) sẽ chuyển sang Result, ? operator, custom error type, thiserror, anyhow — phần xử lý lỗi có thể phục hồi. Hiểu panic! trước là điều kiện để biết khi nào KHÔNG dùng nó.
panic! Là Gì
panic! là macro built-in của Rust, dùng để báo hiệu một lỗi không thể phục hồi. Khi gặp panic!, Rust sẽ:
- In error message ra
stderrkèm file + line number nơi xảy ra panic. - Gợi ý
note: run with `RUST_BACKTRACE=1` ...nếu user chưa bật biến môi trường này; nếu đã bật, in stack trace. - Unwind stack (mặc định) — chạy các destructor (
Drop::drop) của biến trên stack để giải phóng tài nguyên, hoặc abort ngay nếu profile cấu hìnhpanic = "abort". - Kết thúc thread hiện tại với exit code khác 0. Nếu thread đó là main thread → toàn bộ process kết thúc.
fn main() {
let age: i32 = -5;
if age < 0 {
panic!("age không thể âm: {age}");
}
println!("OK, age = {age}");
}
Output khi chạy:
thread 'main' panicked at src/main.rs:4:9:
age không thể âm: -5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Chú ý: panic KHÔNG resume. Không có cách viết try { ... } catch { ... } kiểu Java / Python để bắt panic và "đi tiếp". Ranh giới duy nhất bắt được panic là catch_unwind ở mục 8 — và nó chỉ dùng cho ranh giới đặc biệt như FFI, không phải để giả lập exception.
Khi Nào Dùng panic!
Quy tắc đơn giản: panic chỉ dùng khi lỗi là bug của lập trình viên, không phải lỗi mà người dùng / hệ thống có thể tạo ra hợp pháp.
NÊN dùng panic!
- Invariant violation: một điều kiện đáng lẽ luôn đúng theo thiết kế nhưng bị vi phạm. Ví dụ: hàm
sqrt(x)nhận đầu vào "đã được caller đảm bảo >= 0" nhưng lại nhận giá trị âm → caller đã viết sai → panic là hợp lý. - Contract broken: lập trình viên gọi API sai cách (truyền pointer null vào hàm yêu cầu non-null, sử dụng resource sau khi đã close...). Đây là bug, sửa code chứ không phải retry.
- Prototype / example code: dùng
unwrap()/expect()để code ngắn, đỡ rườm rà. Khi nâng cấp lên production thì refactor sangResult. - Test:
assert!,assert_eq!đều panic khi sai.#[test]coi panic là dấu hiệu test failed. - Trạng thái mà tiếp tục là nguy hiểm: detect được memory corruption, double-free, security invariant bị phá → panic an toàn hơn tiếp tục.
KHÔNG nên dùng panic!
- File không tồn tại, không có quyền đọc → trả
Result<_, io::Error>. - Network timeout, server trả 500 → trả
Result. - Input người dùng sai format — parse số không được, JSON malformed → trả
Result. - Database bị disconnect tạm thời → trả
Resultđể caller retry.
Khái niệm phân biệt: lỗi do bug (bạn cần sửa code) vs lỗi do môi trường (bạn cần xử lý). Panic cho cái đầu, Result cho cái sau. Đây là triết lý xuyên suốt mà toàn bộ Nhóm 19 sẽ mở rộng.
panic! vs Result
Rust chia lỗi thành 2 nhóm rõ ràng — đây là điểm khác biệt cốt lõi với hầu hết ngôn ngữ khác:
- Recoverable error →
Result<T, E>. Caller có cơ hội đọc lỗi, quyết định retry / fallback / propagate / log. Hệ thống vẫn chạy tiếp được. - Unrecoverable error →
panic!. Không có ý nghĩa nào để tiếp tục — code đã ở trạng thái sai. Tốt nhất là kết thúc nhanh và rõ.
Không có ngôn ngữ nào "buộc" lập trình viên phải dùng đúng — đây là kỷ luật. Tuy vậy Result trong Rust là #[must_use], nếu bỏ qua sẽ có warning, và operator ? làm việc propagate nhẹ nhàng đến mức không có lý do gì panic cho lỗi đoán trước được. Trong khi đó Java/C# trộn lẫn checked exception với runtime exception, dễ dẫn đến anti-pattern catch (Exception e) { ... } nuốt mọi thứ.
Một ranh giới quan trọng: nếu bạn viết library, mặc định KHÔNG được panic dù chỉ một input lạ. Library mà panic giữa chừng ép caller phải dùng catch_unwind — rất bất tiện. Library tốt trả Result cho mọi lỗi caller-facing, chỉ panic khi caller phá invariant nội bộ của library.
Implicit Panic Trong Stdlib
Bạn không cần gõ panic! trực tiếp mới bị panic — stdlib có nhiều operation panic ngầm khi gặp đầu vào sai. Đây là danh sách thường gặp:
fn main() {
// 1. Vec / array out-of-bound
let v = vec![1, 2, 3];
let x = v[10]; // PANIC: index out of bounds: len 3, index 10
// 2. String slice cắt giữa multi-byte char
let s = "héllo";
let bad = &s[0..1]; // PANIC: byte index 1 is not a char boundary
// 3. Chia cho 0 (integer)
let a = 10;
let b = 0;
let c = a / b; // PANIC: attempt to divide by zero
// 4. Integer overflow (chỉ panic ở debug profile, wrap ở release)
let big: i32 = i32::MAX;
let next = big + 1; // PANIC ở debug, wrap (overflow) ở release
// 5. unwrap trên None / Err
let opt: Option<i32> = None;
let val = opt.unwrap(); // PANIC: called `Option::unwrap()` on a `None` value
// 6. expect — giống unwrap nhưng kèm message
let res: Result<i32, &str> = Err("oops");
let v2 = res.expect("config required"); // PANIC: config required: "oops"
}
Ghi nhớ: chúng đều là panic! ngầm — kéo theo cùng cơ chế (unwind / abort, có thể catch bằng catch_unwind, in backtrace). Lập trình defensive: ưu tiên v.get(10) trả Option thay vì v[10], s.get(0..1) trả Option thay vì &s[0..1], dùng checked_div / checked_add cho integer khi không tin đầu vào. Pitfall &s[0..1] đã được phân tích chi tiết ở Bài 81: UTF-8 Boundary Panic.
Đặc biệt chú ý integer overflow: ở debug profile, Rust bật check, overflow → panic — giúp phát hiện bug sớm. Ở release profile, check bị tắt vì lý do hiệu năng, overflow → wrap (modulo 2^N). Nghĩa là code có thể chạy đúng trong test nhưng silent bug ở production nếu bạn không xử lý overflow cẩn thận.
panic! Macro Syntax
Macro panic! nhận format string y hệt println! / format! — placeholder {}, {value}, {:?}, {:#?}...
fn divide(a: i32, b: i32) -> i32 {
if b == 0 {
// Format string với captured identifier
panic!("division by zero: a={a}, b={b}");
}
a / b
}
fn check_user(id: u64, name: &str) {
if name.is_empty() {
// Format string positional
panic!("user {} có tên rỗng (invalid state)", id);
}
}
fn process(opt: Option<String>) {
let s = opt.unwrap_or_else(|| {
// panic với Debug format
panic!("input rỗng tại process(), trace = {:?}", std::backtrace::Backtrace::capture());
});
println!("{s}");
}
fn main() {
divide(10, 0);
}
Vài lưu ý:
- Format identifier
{value}chỉ hoạt động với biến trong scope — không dùng được với biểu thức (vẫn phải viết{}+ argument). panic!()không có argument chỉ in chuỗi mặc định"explicit panic". Tránh — luôn kèm message để dễ debug.- Message nên trả lời câu "tại sao panic" + "giá trị nào liên quan". Người đọc backtrace sẽ cảm ơn bạn.
Unwind vs Abort
Khi panic xảy ra, Rust có 2 chiến lược chọn được qua Cargo.toml:
# Cargo.toml
[profile.dev]
panic = "unwind" # mặc định, không cần khai báo
[profile.release]
panic = "unwind" # mặc định
# hoặc
# panic = "abort" # tắt unwind, exit ngay
panic = "unwind" (mặc định)
- Rust chạy stack unwinding: đi ngược stack từ điểm panic về
main, mỗi frame chạyDrop::dropcủa các biến local. Heap được giải phóng, file/socket được đóng, mutex được release. - Cho phép
catch_unwindbắt lại panic — ranh giới này chỉ tồn tại khi unwind. - Binary lớn hơn vì compiler phải sinh unwinding table.
panic = "abort"
- Khi panic, gọi
std::process::abort()ngay — process kết thúc, OS thu hồi tài nguyên. - Không chạy destructor — nếu có tài nguyên cần cleanup (xoá file tạm, gửi log cuối), chúng sẽ bị mất.
catch_unwindKHÔNG hoạt động — không có gì để catch.- Binary nhỏ hơn vì bỏ unwinding table. Phù hợp embedded, WASM, microservice không cần cleanup ở cấp process.
Khuyến nghị: giữ unwind cho app thông thường, chuyển sang abort khi cần binary nhỏ và đã chắc chắn không cần cleanup ở cấp panic. Nhiều dự án embedded chọn abort mặc định.
catch_unwind — Catch Panic
std::panic::catch_unwind cho phép chạy một closure và bắt panic phát sinh trong closure đó, trả về Result<T, Box<dyn Any + Send>>. Chỉ hoạt động khi profile là unwind.
use std::panic;
// Use case 1: FFI boundary — Rust gọi từ C, panic vượt qua FFI là UB
#[no_mangle]
pub extern "C" fn my_c_api(input: i32) -> i32 {
let result = panic::catch_unwind(|| {
// Code Rust ở trong, có thể panic
if input < 0 {
panic!("input phải >= 0");
}
input * 2
});
match result {
Ok(v) => v,
Err(_) => -1, // báo lỗi qua return value cho phía C
}
}
// Use case 2: test framework
fn run_test<F: FnOnce() + panic::UnwindSafe>(name: &str, test: F) {
let result = panic::catch_unwind(test);
match result {
Ok(()) => println!("[PASS] {name}"),
Err(_) => println!("[FAIL] {name}"),
}
}
fn main() {
run_test("test_a", || assert_eq!(1 + 1, 2));
run_test("test_b", || assert_eq!(1 + 1, 3)); // panic, FAIL
println!("Tất cả test đã chạy xong, process vẫn sống");
}
Quan trọng — đọc kỹ:
catch_unwindKHÔNG phảitry / catchgeneric-purpose. KHÔNG dùng để xử lý lỗi business logic. Lỗi business →Result.- Use case chính: (a) FFI — panic vượt qua ranh giới Rust ↔ C là undefined behavior, phải catch lại trong wrapper Rust trước khi return cho C. (b) Test framework — chạy nhiều test, một test panic không được kéo theo cả process chết. (c) Long-running server — tránh 1 request panic kéo cả thread pool xuống (mặc dù Rust async runtime như Tokio đã catch sẵn ở task boundary).
- Closure phải
UnwindSafe— đảm bảo trạng thái nội bộ không bị "đứt giữa chừng". Nhiều type không tự độngUnwindSafe; phải wrap trongAssertUnwindSafenếu chắc chắn an toàn. - Không hoạt động khi
panic = "abort". Library dùngcatch_unwindmà user app cấu hìnhabort→ process vẫn chết.
Backtrace Với RUST_BACKTRACE
Mặc định panic chỉ in message + file:line. Để có stack trace, bật biến môi trường RUST_BACKTRACE trước khi chạy:
# Linux / macOS
RUST_BACKTRACE=1 cargo run
# Backtrace đầy đủ hơn (kèm cả frame của std và panic handler)
RUST_BACKTRACE=full cargo run
# Windows PowerShell
$env:RUST_BACKTRACE = "1"
cargo run
# Windows cmd
set RUST_BACKTRACE=1
cargo run
Output mẫu khi bật RUST_BACKTRACE=1:
thread 'main' panicked at src/main.rs:4:9:
age không thể âm: -5
stack backtrace:
0: rust_begin_unwind
1: core::panicking::panic_fmt
2: my_app::check_age
at ./src/main.rs:4:9
3: my_app::main
at ./src/main.rs:12:5
...
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
Lưu ý thực hành:
- Backtrace chỉ useful khi build có debug symbol — debug profile mặc định có, release profile mặc định không. Để có backtrace ở release, set
debug = truetrong[profile.release]hoặcstrip = false. - Production server nên log panic kèm backtrace đầy đủ. Cách thường gặp: set
RUST_BACKTRACE=1trong systemd unit / Dockerfile / CI env. - Crate
color-eyre/anyhowkết hợp backtrace của panic và backtrace củaResulterror trong cùng một cơ chế — sẽ học chi tiết ở các bài cuối Nhóm 19. - Có thể chủ động capture trong code bằng
std::backtrace::Backtrace::capture()— không cần env var, nhưng cần build có debug info.
Tổng Kết
panic!abort thread hiện tại, in error + (tuỳRUST_BACKTRACE) backtrace, không cho resume execution.- Dùng panic cho bug của lập trình viên: invariant violation, contract broken, prototype, test. KHÔNG dùng cho lỗi đoán trước được (file not found, network, parse input) — những lỗi đó thuộc về
Result. - Implicit panic phổ biến:
v[10],&s[0..1]cắt UTF-8, chia 0 integer, overflow ở debug,unwrap()trênNone/Err. Dùngget,checked_*để tránh. - Macro syntax giống
println!— luôn kèm message giải thích "tại sao" và biến liên quan. panic = "unwind"(default) chạy destructor và cho phépcatch_unwind;panic = "abort"exit ngay, binary nhỏ hơn, mất cleanup.catch_unwindchỉ dùng ở FFI boundary, test framework, server thread isolation — KHÔNG dùng nhưtry / catchgeneric.RUST_BACKTRACE=1hoặcfullđể in stack trace khi debug; production nên bật sẵn.
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- Bạn viết hàm
fn read_config(path: &str) -> Configđọc file YAML và parse. Nên trả thẳngConfigvà panic khi file thiếu, hay trảResult<Config, MyError>? Vì sao? - Code
let x = vec![1,2,3][10];panic. Viết lại bằnggetđể không panic và in message thân thiện nếu index sai. - Trong CI bạn thấy job test fail với log "thread 'main' panicked at..." nhưng không có stack trace. Cần thay đổi gì để có backtrace?
- Bạn viết FFI export
extern "C" fn rust_compute(...). Vì sao bên trong phải bọccatch_unwind? - Trong release profile, code
let x: i32 = i32::MAX + 1;sẽ làm gì? Trong debug thì sao?
Đáp án
- Trả
Result<Config, MyError>. File config có thể thiếu, sai format, không có quyền đọc — đều là lỗi môi trường, KHÔNG phải bug lập trình viên. Caller có quyền chọn fallback (dùng default config), retry, hoặc thông báo cho user. Hàm panic ép caller phảicatch_unwindhoặc chấp nhận chết process — bất lịch sự. match vec![1,2,3].get(10) { Some(&x) => println!("{x}"), None => eprintln!("index out of range") }—gettrảOption<&T>, không panic.- Set
RUST_BACKTRACE=1(hoặcfull) trong CI env. Ngoài ra, nếu chạy release thì cầndebug = truetrong[profile.release]để có debug symbol — không có symbol thì backtrace chỉ là địa chỉ raw. - Panic vượt qua ranh giới Rust ↔ C là undefined behavior theo spec. Phía C không có cơ chế stack unwinding tương thích — process có thể crash, corrupt memory, hoặc tệ hơn là silent bug. Bọc
catch_unwindđể chặn panic ngay trong Rust, sau đó convert sang error code / sentinel value trả về cho C. - Trong debug: panic với "attempt to add with overflow" — Rust bật check overflow để bắt bug sớm. Trong release: wrap modulo 2^32 —
i32::MAX + 1trở thànhi32::MIN(-2147483648), không panic. Bug silent. Để consistent giữa 2 mode, dùngchecked_add(trảOption),wrapping_add(luôn wrap), hoặcsaturating_add(clamp vềMAX/MIN).
Bài Tiếp Theo
Bài 141: Result<T,E> — Recoverable Error — chuyển sang phía kia của error handling: enum Result<T, E> với 2 variant Ok(T) / Err(E), chuẩn của Rust cho fallible function. Học pattern match Result, các method map / and_then / map_err, ý nghĩa #[must_use], và preview operator ? (sẽ học sâu ở Bài 143). Sau bài 141 bạn sẽ thấy hầu hết function trong stdlib (đọc file, parse số, network) đều trả Result — và biết cách handle gọn.
