ข้ามไปยังเนื้อหา

Panics และ Unwrap

Panic คือวิธีที่ Rust บอกว่า “สถานการณ์นี้คือบั๊ก — ฉันไม่สามารถดำเนินการต่อได้อย่างปลอดภัย” เมื่อ panic เกิดขึ้น Rust จะคลายสแต็กการเรียก รัน destructor และยุติเธรดปัจจุบัน ในโปรแกรมแบบ single-thread หมายความว่าโปรเซสทั้งหมดจะออก

คุณสามารถทริกเกอร์ panic อย่างชัดเจนด้วย macro panic!:

// @expect-panic
fn main() {
panic!("something went terribly wrong");
}

หลายการดำเนินการมาตรฐานยัง panic โดยอัตโนมัติเมื่อใช้ไม่ถูกต้อง:

// @expect-panic
fn main() {
let v = vec![1, 2, 3];
println!("{}", v[10]); // index out of bounds — panic!
}
// @expect-panic
fn main() {
let x: Option<i32> = None;
let _ = x.unwrap(); // called unwrap() on a None value — panic!
}

คอมไพเลอร์พูดว่าอะไร: รูปแบบข้อความ Panic ของ 1.96

หัวข้อที่มีชื่อว่า “คอมไพเลอร์พูดว่าอะไร: รูปแบบข้อความ Panic ของ 1.96”

ถ้อยคำที่แน่นอนของข้อความ panic ไม่ใช่แค่เรื่องความสวยงาม — มันคือสิ่งที่คุณจะ paste ลงใน bug report หรือ grep หาใน log บน rustc/cargo 1.96 รูปแบบข้อความมี thread id เป็นตัวเลขอยู่ในวงเล็บต่อจากชื่อเธรด:

// @expect-panic
fn main() {
let x: Option<i32> = None;
let _ = x.unwrap();
}
thread 'main' (41674254) panicked at p2.rs:3:15:
called `Option::unwrap()` on a `None` value
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

thread 'main' (<id>) panicked at <file>:<line>:<col>: คือหัวข้อที่ panic ทุกครั้งพิมพ์ ตามด้วยข้อความ panic ในบรรทัดถัดไป .unwrap() บน None จะพิมพ์ called \Option::unwrap()` on a `None` valueเสมอ — ไม่มีข้อความกำหนดเอง ซึ่งเป็นเหตุผลที่.expect()` มีอยู่

.unwrap() บน Option<T> หรือ Result<T, E> ดึงค่าภายในหากเป็น Some/Ok และpanic หากเป็น None/Err เป็นทางลัดที่แลกความปลอดภัยกับความกระชับ

.expect(msg) ทำสิ่งเดียวกัน แต่ให้คุณกำหนดข้อความอธิบายที่ปรากฏในผลลัพธ์ panic ทำให้การ debug ง่ายขึ้นมาก

fn main() {
let some_value: Option<i32> = Some(42);
let n = some_value.unwrap(); // fine — value is Some
println!("n = {}", n);
let parsed: Result<i32, _> = "99".parse();
let m = parsed.expect("input should always be a valid integer");
println!("m = {}", m);
}

ข้อความ .expect() ที่ดีต้องบอกเหตุผลว่าทำไมค่าคงที่ควรเป็นจริง ไม่ใช่แค่บอกว่าอะไรล้มเหลว — เพื่อให้ใครก็ตามที่อ่านผลลัพธ์ panic (บ่อยครั้งคือตัวคุณเองในอนาคต ตอนตีสาม) รู้ทันทีว่านี่คือบั๊กจริงหรือเป็นสมมติฐานที่เพิ่งพัง ธรรมเนียมของชุมชน Rust คือเขียนเป็นประโยคที่ระบุค่าคงที่ ในรูป “should be X because Y” หรือ “this X should exist”:

fn main() {
let env_value = std::env::var("PATH");
// Bad: restates the operation, tells you nothing you didn't know
// env_value.expect("failed to get env var");
// Good: states the invariant and why it should hold
let _path = env_value.expect("PATH should be set by the shell that launched this process");
println!("PATH lookup succeeded");
}

.unwrap() และ .expect() ทุกตัวในโค้ดเบสคือคำกล่าวอ้างว่า “ตรงนี้ล้มเหลวไม่ได้” ก่อน merge โค้ด — ไม่ว่าจะเป็นของคุณเองหรือของ AI assistant — ให้ grep หาพวกมันและตรวจสอบแต่ละตัวกับคำกล่าวอ้างนั้น:

Terminal window
grep -rn 'unwrap()' src/

สำหรับแต่ละจุดที่เจอ ให้ถามว่า None/Err เป็นไปไม่ได้จริงๆ ตามทุกอย่างที่คอมไพเลอร์รู้ ณ จุดนี้ หรือมันขึ้นอยู่กับข้อเท็จจริงภายนอก (ไฟล์ที่มีอยู่ การเรียกเครือข่ายที่สำเร็จ) ที่เปลี่ยนแปลงได้? ถ้าเป็นแบบหลัง มันควรอยู่ฝั่ง Result ของตารางตัดสินใจ ไม่ใช่อยู่หลัง .unwrap() clippy::unwrap_used (lint ประเภท restriction ปิดโดยค่าเริ่มต้น) สามารถบังคับสิ่งนี้โดยอัตโนมัติใน library crate ได้ — โมดูล concurrency-testing-cargo ครอบคลุมการรันมัน

Panic เหมาะสมเมื่อ:

  • สถานการณ์แสดงถึงบั๊กในโค้ดของคุณ ไม่ใช่ความล้มเหลวของผู้ใช้หรือสภาพแวดล้อม
  • คุณกำลังเขียนการทดสอบหรือโปรโตไทป์ ที่การ crash อย่างรวดเร็วมีประโยชน์
  • คุณได้ตรวจสอบเงื่อนไขเบื้องต้นไว้ที่อื่นแล้ว และค่าคงที่ไม่สามารถถูกละเมิดได้จริง

Panic ไม่เหมาะสมเมื่อ:

  • ความล้มเหลวสามารถมาจากอินพุตภายนอก (ไฟล์ เครือข่าย ข้อมูลผู้ใช้)
  • ผู้เรียกอาจต้องการกู้คืนและลองสิ่งอื่น
  • คุณกำลังเขียนโค้ดไลบรารีที่โปรแกรมอื่นเรียกใช้

ค่าเริ่มต้นที่ปลอดภัยกว่าด้วย .unwrap_or และ .unwrap_or_else

หัวข้อที่มีชื่อว่า “ค่าเริ่มต้นที่ปลอดภัยกว่าด้วย .unwrap_or และ .unwrap_or_else”

เมื่อคุณต้องการค่าสำรองแทนการ panic ให้ใช้ .unwrap_or(default) หรือ .unwrap_or_else(|| compute_default())

fn safe_sqrt(n: f64) -> Result<f64, String> {
if n < 0.0 {
Err(format!("cannot take sqrt of negative number: {}", n))
} else {
Ok(n.sqrt())
}
}
fn main() {
// .unwrap_or provides a default instead of panicking
let a: Option<i32> = Some(42);
let b: Option<i32> = None;
println!("a unwrap_or: {}", a.unwrap_or(0));
println!("b unwrap_or: {}", b.unwrap_or(0));
// .expect gives a custom panic message (use only when failure is a bug)
let good = safe_sqrt(16.0).expect("sqrt should not fail on positive numbers");
println!("sqrt(16) = {}", good);
// .unwrap_or_else with a closure for recoverable defaults
let result = safe_sqrt(-4.0).unwrap_or_else(|e| {
println!("Handled: {}", e);
0.0
});
println!("result = {}", result);
}

โดยค่าเริ่มต้น Rust binary จะ unwind เมื่อ panic: มันไต่กลับขึ้นสแต็กการเรียก รัน Drop impl ทุกตัวระหว่างทางก่อนที่โปรเซสจะออก คุณสามารถเลือกใช้ abort แทนได้ — โปรเซสจะยุติทันที ไม่มีการ unwind และไม่มี destructor รันเลย — โดยตั้งค่า profile ใน Cargo.toml:

[profile.release]
panic = "abort"

abort ให้ binary ที่เล็กกว่า (ไม่มีตาราง unwinding) และจำเป็นสำหรับบาง embedded target แต่มันหมายความว่าการรับประกันของ Drop จะหยุดใช้ได้ทันทีที่ panic เริ่มขึ้น การรันจริงสองครั้งด้านล่างใช้โปรแกรมเดียวกัน — Guard ที่ Drop ของมันพิมพ์ข้อความ — คอมไพล์ครั้งหนึ่งด้วยกลยุทธ์ unwind ค่าเริ่มต้น และอีกครั้งด้วย panic = "abort":

$ rustc --edition 2024 unwind_drop.rs -o unwind_drop && ./unwind_drop; echo "exit=$?"
thread 'main' (41688821) panicked at unwind_drop.rs:9:5:
boom
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
dropping guard
exit=101
$ cargo build --quiet && ./target/debug/abort-scratch; echo "exit=$?" # Cargo.toml has panic = "abort"
thread 'main' (41687919) panicked at src/main.rs:9:5:
boom
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
exit=134

ภายใต้ unwind บรรทัด dropping guard จะพิมพ์ก่อนที่โปรเซสจะออกด้วย exit code 101 ภายใต้ abort บรรทัดนั้นไม่ถูกพิมพ์เลย และ exit code กลายเป็น 134 (128 + SIGABRT) — OS ฆ่าโปรเซสไปแล้ว Rust ไม่มีโอกาสได้รัน destructor ใดๆ เลย

std::panic::catch_unwind ให้คุณหยุด unwind กลางทางขึ้นสแต็กแล้วรันต่อได้ — มันทำงานได้กับกลยุทธ์ unwind เท่านั้น ไม่เคยทำงานกับ abort:

fn main() {
let result = std::panic::catch_unwind(|| {
println!("about to panic");
panic!("caught you");
});
match result {
Ok(_) => println!("no panic"),
Err(_) => println!("recovered from a panic"),
}
println!("program continues");
}

std::process::exit(code) ยุติโปรเซสทันที เหมือนกับ panic = "abort" ในแง่นี้ทุกประการ: ไม่มี destructor รันเลย แม้แต่ค่าที่ยังอยู่ใน scope ของ main เพราะไม่มีการ unwind เกิดขึ้นเลย — โปรเซสแค่หยุด

// @no-run exits with code 3, not 0 — see the real transcript in prose below
struct Guard(&'static str);
impl Drop for Guard {
fn drop(&mut self) {
println!("Dropping guard: {}", self.0);
}
}
fn main() {
let _g1 = Guard("normal scope end");
{
let _g2 = Guard("about to exit early");
println!("calling process::exit(3)");
std::process::exit(3);
}
}
$ rustc --edition 2024 exit_demo.rs -o exit_demo && ./exit_demo; echo "exit=$?"
calling process::exit(3)
exit=3

การรันโค้ดนี้พิมพ์แค่ calling process::exit(3) และออกด้วย exit code 3 — ทั้ง _g1 และ _g2 ไม่มี Drop ตัวไหนถูกรันเลย แม้ _g1 จะยังอยู่ใน scope อยู่มากก็ตาม ถ้า Drop impl มีหน้าที่ flush file buffer, ปล่อย lock, หรือปิด network connection process::exit จะข้ามทั้งหมดนั้นแบบเงียบๆ ให้ใช้ return ปกติจาก main (หรือปล่อยให้จบท้ายฟังก์ชันไปเอง) เมื่อไหร่ก็ตามที่คุณต้องการให้ destructor เหล่านั้นรัน

flowchart TD
  P[panic! fires] --> S{Panic strategy}
  S -->|unwind - default| U[Walk stack,
run every Drop]
  U --> T[Thread exits
(process exits, 101, if main thread)]
  S -->|abort - Cargo.toml| A[Process terminates
immediately, no Drop]
  A --> X[Exit via SIGABRT, 134]
  E[std::process::exit] --> A
Unwind เทียบกับ abort เทียบกับ process::exit

Panic คือ Rust ตัดสินใจแทนคุณว่าสิ่งที่ปลอดภัยที่สุดที่จะทำคือหยุดแทนที่จะเดาต่อไป Unwinding คือวิธี “สุภาพ” ในการหยุด — มันให้โอกาสทุกค่าบนสแต็กได้ทำความสะอาดผ่าน Drop ก่อนที่เธรดจะจบ abort และ process::exit คือวิธี “ไม่สุภาพ” — มันหยุด ทันที และการทำความสะอาดใดๆ ที่ควรเกิดใน destructor ก็แค่ไม่เกิดขึ้นเลย การเลือก .unwrap() ในโค้ดของคุณคือการเลือกเวอร์ชันสุภาพของ “หยุดทันที” การเลือก process::exit ที่ไหนก็ตามนอกเหนือจากบรรทัดสุดท้ายของ main คือการเลือกเวอร์ชันไม่สุภาพ ซึ่งมักจะไม่ได้ตั้งใจ

การ unwind มีต้นทุนขนาด binary (ตาราง unwind) และ overhead รันไทม์เล็กน้อยบน happy path ใน ABI บางแบบ แต่มันรักษาการทำความสะอาดแบบ Drop ไว้ และเปิดใช้ catch_unwind ที่ขอบเขต FFI หรือ thread pool abort เล็กกว่าและจำเป็นสำหรับบาง no-std/embedded target แต่ Drop ใดๆ ที่โปรแกรมของคุณพึ่งพาเพื่อความถูกต้อง (flush, ปล่อย lock, ปิด socket) จะหยุดเกิดขึ้นแบบเงียบๆ ทันทีที่คุณเปลี่ยนไปใช้มัน process::exit ให้คุณควบคุม exit code ได้แม่นยำจากที่ไหนก็ได้ในโปรแกรม โดยแลกกับพฤติกรรมข้าม Drop แบบเงียบๆ เหมือนกับ abort ทุกประการ — เรียกใช้เฉพาะเมื่อแน่ใจว่าไม่มีอะไรต้องทำความสะอาดอีกแล้ว

  • “.expect("เหตุผลสั้นๆ") ดีกว่า .unwrap() เสมอ” ดีกว่าจริงก็ต่อเมื่อข้อความบอกค่าคงที่จริงๆ .expect("failed") แทบไม่มีประโยชน์มากไปกว่า .unwrap() เปล่าๆ — ธรรมเนียมคือ “should be X because Y” ไม่ใช่การพูดซ้ำชื่อการดำเนินการ
  • “catch_unwind ทำให้ panic ปลอดภัยที่จะเพิกเฉย เหมือน try/catch ในภาษาอื่น” มันเป็นทางออกแคบๆ สำหรับ FFI และการแยก worker ไม่ใช่ control flow ทั่วไป — โค้ดที่ panic อาจทิ้งค่าคงที่ไว้พัง และ catch_unwind ไม่ได้ซ่อมแซมอะไรเลย
  • “process::exit เป็นแค่ return จาก main ที่เร็วกว่า” มันข้าม destructor ที่ค้างอยู่ทั้งหมดในโปรแกรมทั้งหมด ไม่ใช่แค่ใน main — รวมถึง guard, file handle และการปล่อย lock ที่ไหนก็ตามบนสแต็ก ไม่ใช่แค่ในฟังก์ชันที่เรียก

AI มักจะเลือกใช้ .expect("") ด้วยสตริงว่างหรือใกล้ว่าง หรือพูดซ้ำชื่อการดำเนินการกลับมา เพราะมันผ่าน type checker ได้โดยที่ผู้เขียนไม่ได้คิดจริงๆ ว่าทำไมค่าคงที่ควรเป็นจริง

🐛 หา bug ในโค้ดนี้

fn get_admin_email(config: &Config) -> String {
config.admin_email.clone().expect("")
}
เฉลย

ข้อความ .expect("") ที่ว่างเปล่าให้ข้อมูลไม่มากไปกว่าข้อความ panic ของ .unwrap() เปล่าๆ — คุณเสียสิ่งเดียวที่ .expect() มีไว้ให้ ที่แย่กว่านั้นคือมันซ่อนเหตุผลว่าทำไม admin_email ถูกสมมติว่ามีอยู่เสมอ วิธีแก้ตามธรรมเนียมข้อความในบทนี้คือ .expect("admin_email should be set because Config::load validates it at startup") — หรือถ้าฟิลด์นี้ไม่มีอยู่ได้จริงๆ ให้คืนค่า Result/Option แทนการ panic ตรงนี้เลย

อะไรเกิดขึ้นเมื่อคุณเรียก .unwrap() บนค่า None?
ความแตกต่างระหว่าง .unwrap() และ .expect(msg) คืออะไร?
สถานการณ์ใดที่เป็นการใช้ panic! ที่ถูกต้อง?
โปรแกรมนี้ print อะไร และด้วย exit code เท่าไร?
struct Guard;
impl Drop for Guard {
    fn drop(&mut self) { println!("dropping guard"); }
}
fn main() {
    let _g = Guard;
    std::process::exit(3);
}
.unwrap_or(default) ทำอะไรเมื่อ Option เป็น None?
เมื่อเทียบกับกลยุทธ์ panic แบบ unwind ค่าเริ่มต้น อะไรเปลี่ยนไปเมื่อ Cargo.toml ตั้ง panic = "abort"?