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

การ​จัดการ error — Result, ตัว​ดำเนิน​การ `?`, panic กับ thiserror

ใน​โลก C#/.NET ความ​ผิดพลาด​มี​ทาง​เดิน​เดียว: โยน Exception ขึ้น​ไป​แล้ว​หวัง​ว่า​จะ​มี​ใคร catch ทัน — file อ่าน​ไม่​ได้, แปลง​ตัวเลข​พลาด, หรือ NullReferenceException ที่​ไม่มี​ใคร​ประกาศ​ไว้​ใน​ลายเซ็น ล้วน​ไหล​ผ่าน​ช่อง​ทาง​เดียวกัน​หมด และ compiler ไม่​เคย​บังคับ​ให้​คุณ​จัดการ​มัน​เลย Rust เดิน​คนละ​ทาง: ความ​ผิดพลาด​ที่ กู้​ได้ ไม่ใช่​การกระโดด​ออก​จาก​กระแส​โปรแกรม แต่​เป็น ค่า​ธรรมดา ที่ function คืน​กลับ​มา — ชนิด ResultResult`Ok(T)` หรือ `Err(E)` — error เป็น​ค่า​ปกติ​ใน​ลายเซ็น ไม่ใช่ exception — และ compiler จะ​ไม่​ยอม​ให้​คุณ​เมิน​มัน​เงียบๆ

นี่​คือ​หนึ่ง​ใน​ความ​ต่าง​ที่​วิศวกร C# รู้สึก​ได้​ชัด​ที่สุด: error กลาย​เป็น​ส่วน​หนึ่ง​ของ type signature ไม่ใช่​ผล​ข้าง​เคียง​ที่​ซ่อน​อยู่ บท​นี้​เรา​จะ​เดิน​จาก Result<T,E> เปล่าๆ ไป​จนถึง error type จริง​ของ taskcli ที่​สร้าง​ด้วย thiserrorthiserrorderive macro สร้าง `Error`+`Display` ให้ error enum ของ library (crate v2.x) และ​ปิด​ท้าย​ด้วย​เส้น​แบ่ง thiserror (สำหรับ library) กับ anyhow (สำหรับ application)

📦 kaen-taskcli

code ลงมือ​ของ​คอร์ส​นี้​อยู่​ใน repo kaen-taskcli (code ตัวอย่าง​กำลัง​จัด​ทำ) — บท​นี้​เรา​ออกแบบ error type ของ taskcli: enum ชื่อ ConfigError ที่​รวม​ความ​ผิดพลาด​จาก​การ​อ่าน file และ​การ parse ค่า​เข้า​ไว้​ด้วย​กัน​แบบ matchable code ? และ Box<dyn Error> ใน​บท​นี้ compile ผ่าน​จริง​บน rustc 1.97.1 (edition 2024) ส่วน​สองสนิปเป็ต thiserror/anyhow เป็น derive-macro ที่​เรา​คัด​มา​จาก version ที่ verify แล้ว — versions ที่ pin (thiserror 2.0.19, anyhow 1.0.104, as-of 2026-07-18) resolve ได้​จริง

หนังสือ The Rust Programming Language (ch9) แบ่ง​ความ​ผิดพลาด​ออก​เป็น​สอง​กลุ่ม​ใหญ่​ชัดเจน และ​นี่​คือ​แกน​ของ​ทั้ง​บท:

  • กู้​ได้ (recoverable) — สิ่ง​ที่ คาด​ว่า​จะ​เกิด​ได้ ใน​การ​ทำงาน​ปกติ: file ไม่มี​อยู่, input ผิด​รูปแบบ, port ที่​ผู้​ใช้​พิมพ์​ไม่ใช่​ตัวเลข Rust แทน​ด้วย Result<T, E> = Ok(T) หรือ Err(E) — เป็น ค่า ที่​คืน​กลับ​มา​ให้​คุณ​ตัดสิน​ใจ
  • กู้​ไม่​ได้ (unrecoverable) — bug หรือ invariant ที่​พัง​จน​ไม่​ควร​ทำงาน​ต่อ: index เกิน​ขอบ array, logic ที่ “เป็น​ไป​ไม่​ได้” ดัน​เกิด ตรง​นี้​ใช้ panic!panic!error ที่​กู้​ไม่​ได้ (bug/invariant พัง) ทำให้ thread unwind; ไม่ใช่​ทาง​ไหล​ของ error ปกติ ซึ่ง​ทำให้ thread unwind (คลาย stack แล้ว​เรียก drop ให้​ทุก​ค่า) หรือ abort ทันที

ความ​ต่าง​ที่​ต้อง​ฝังหัว: panic! ไม่ใช่ ช่อง​ทาง​ไหล​ของ error ปกติ มัน​คือ​สัญญาณ​ว่า “โปรแกรม​นี้​มี bug” ไม่ใช่ “ผู้​ใช้​พิมพ์​ผิด” ใน C# คุณ​ใช้ throw/catch กับ​ทั้ง​สอง​กรณี​ปน​กัน — Rust จงใจ​แยก​ออก​จาก​กัน

และ​นี่​คือ​จุด​ที่ compiler ทำ​หน้าที่​ครู: Result ถูก​ทำ​เครื่องหมาย #[must_use] แปล​ว่า​ถ้า​คุณ​เรียก function ที่​คืน Result แล้ว ทิ้ง​ค่า​เฉยๆ compiler จะ warn ทันที — คุณ ทิ้ง error เงียบๆ ไม่​ได้ ต่าง​จาก C# ที่​ลืม try/catch แล้ว​โปรแกรม​พัง​ตอน runtime โดย​ไม่มี​ใคร​เตือน​ตอน compile

C# / .NETRust…ยกเว้น (สะพาน ไม่ใช่​ของ​เดียวกัน)
Exception ช่อง​ทาง​เดียว​สำหรับ​ทุก​ความ​ผิดพลาดResult<T,E> (กู้​ได้) แยก​จาก panic! (bug)error ที่​กู้​ได้​เป็น ค่า ใน​ลายเซ็น ไม่ใช่​การกระโดด​ที่​ซ่อน​อยู่ — ไม่มี checked/unchecked เพราะ E อยู่​ใน​ชนิด​เสมอ
ลืม try/catch → พัง​ตอน runtime เงียบๆResult เป็น #[must_use] → ทิ้ง​เฉยๆ ได้ warningcompiler รักษา ให้​คุณ​ต้อง​รับรู้ error ทุก​ตัว​ตั้งแต่​ตอน compile
throw แพง​เพราะ​เก็บ stack trace เสมอคืน Err(e) ราคา​เท่ากับ return ค่า​ธรรมดาไม่มี stack unwinding ใน​ทาง​ไหล​ปกติ — error เป็น​แค่​ค่าที่​ส่ง​กลับ

Result มี​ทาง​ลัด​สอง​ตัว​ที่​แกะ​ค่า Ok ออก​มา​ให้ แต่ panic ถ้า​เป็น Err:

  • .unwrap() — คืน​ค่า​ใน Ok หรือ panic พร้อม​ข้อความ​ปริยาย
  • .expect("…") — เหมือน​กัน แต่​คุณ​เขียน ข้อความ​ที่​อธิบาย​สมมติฐาน เอง
fn parse_priority(s: &str) -> Result<u8, std::num::ParseIntError> {
let n: u8 = s.trim().parse()?;
Ok(n)
}
fn main() {
// unwrap/expect: panic ถ้าเป็น Err — ยอมรับได้เมื่อรู้แน่ว่าพลาดไม่ได้
let level: u8 = "3".parse().expect("literal นี้เป็นตัวเลขแน่นอน");
println!("level = {level}");
// ทางปกติ: จับ Result ด้วย match แล้วตัดสินใจเอง
match parse_priority("not-a-number") {
Ok(n) => println!("priority = {n}"),
Err(e) => println!("อ่าน priority ไม่ได้: {e}"),
}
}

unwrap/expect ยอมรับ​ได้ ใน​สาม​ที่: code test, prototype ที่​รีบๆ และ​จุด​ที่​คุณ พิสูจน์​ได้​ว่า​เป็น Err ไม่ได้จริงๆ (เช่น parse literal "3" ข้าง​บน) .expect() ดี​กว่า .unwrap() เสมอ​เพราะ​ข้อความ​บอก​คน​อ่าน (และ​ตัว​คุณ​เอง​ใน​อนาคต) ว่า​คุณ อ้าง อะไร​ไว้ การ​โปรย unwrap ทั่ว code production เทียบ​ได้​กับ​การ catch แล้ว​กลืน exception เงียบๆ ใน C# — คุณ​เปลี่ยน error ที่​จัดการ​ได้​ให้​กลาย​เป็น crash

ทีนี้​ลอง​ส่ง​ต่อ error ขึ้น​ไป​ให้​ผู้​เรียก​จัดการ ❌ version ดิบ ที่​เขียน match ด้วย​มือ​ทุก​ครั้ง​จะ​บวม​เร็ว​มาก:

fn read_username_raw(path: &str) -> Result<String, std::io::Error> {
let name = match std::fs::read_to_string(path) {
Ok(text) => text,
Err(e) => return Err(e), // ส่งต่อ error เอง
};
Ok(name.trim().to_string())
}

? operator? operatorตัว​ช่วย​ส่ง​ต่อ error: คืน​ค่า Ok/Some หรือ early-return Err/None พร้อม​แปลง error ด้วย `From` ยุบ pattern ทั้ง​ก้อน​นั้น​ให้​เหลือ​อักขระ​เดียว — วาง​ท้าย expression ที่​คืน Result: ถ้า​เป็น Ok(v) มัน แกะ v ออก​มา​แล้ว​ทำงาน​ต่อ; ถ้า​เป็น Err(e) มัน early-return ออก​จาก function ทันที​พร้อม​ค่า​นั้น:

use std::error::Error;
use std::fs;
fn read_username(path: &str) -> Result<String, std::io::Error> {
let name = fs::read_to_string(path)?;
Ok(name.trim().to_string())
}
fn main() -> Result<(), Box<dyn Error>> {
let user = read_username("user.txt")?;
println!("user = {user}");
Ok(())
}

สอง​อย่าง​ที่​ต้อง​รู้​เกี่ยว​กับ ?:

  1. มัน​ทำงาน เฉพาะ ใน function ที่ return type เข้า​กัน​ได้ — Result, Option, หรือ​ชนิด​ที่ implement FromResidual ใส่ ? ใน function ที่​คืน () เฉยๆ ไม่​ได้
  2. ตอน early-return มัน แปลง​ชนิด error ให้​อัตโนมัติ​ด้วย FromFromtrait แปลง​ชนิด; impl `From` แล้ว​ได้ `Into` ฟรี และ​เป็น​กลไก​ที่ `?` ใช้​แปลง error — ถ้า E ตัว​จริง​ต่าง​จาก E ใน​ลายเซ็น ? จะ​เรียก From::from แปลง​ให้​ก่อน​คืน ตอน​นี้​ให้​จำ​แค่ ผลลัพธ์​เชิง​ปฏิบัติ ว่า “? แปลง error ให้”; ในบท6 เรา​จะ​เปิดเผย​ว่า​กลไก​นี้ คือ trait From ที่​คุณ impl เอง​ได้
C# ไม่มี​อะไร​เทียบ ? ตรงๆ

ที่​ใกล้​ที่สุด​คือ exception ที่ bubble up เอง​โดย​ไม่​ต้อง​เขียน catch — แต่ ? ต่าง​ตรง​ที่​มัน มอง​เห็น​ได้​ใน code (อักขระ ? คา​ตา​อยู่) และ บังคับ ให้ function ประกาศ error type ใน​ลายเซ็น ไม่มี error ตัว​ไหน “ลอย” ขึ้น​ไป​โดยที่​ผู้​อ่าน​ลายเซ็น​ไม่รู้ตัว การ​ส่ง​ต่อ error จึง​ชัดเจน​และ type-checked ทุก​ก้าว

สังเกต​ลายเซ็น fn main() -> Result<(), Box<dyn Error>> ข้าง​บน — นี่​คือ​สำนวน​มาตรฐาน​ของ binary/app ที่​อยาก​ใช้ ? ใน main ได้​เลย Box<dyn Error> คือ “error ชนิด​ไหน​ก็ได้​ที่ implement trait Error” ห่อ​ไว้​หลัง pointer — มัน​รับ io::Error, ParseIntError, หรือ​อะไร​ก็ตาม​ที่​เป็น Error ได้​หมด ทำให้ ? ส่ง​ต่อ error ต่าง​ชนิด​ใน function เดียว​ได้

เมื่อ main คืน Err(...) runtime จะ​พิมพ์ error ใน​รูปแบบ Debug (ไม่ใช่ Display) แล้ว exit ด้วย code 1 — สะดวก​สำหรับ CLI

Box<dyn Error> เหมาะ​กับ app ไม่​เหมาะ​กับ public API ของ library

Box<dyn Error> ลบ​ชนิด (type erasure) ทิ้ง​ไป — ผู้​เรียก​จับ​ได้​แค่​ว่า “มี error บาง​อย่าง” แต่ match แยก​กรณี​ไม่​ได้​อีก​ต่อ​ไป มัน​เทียบ​ได้​กับ​การ catch (Exception e) ที่​ฐาน​สุด​ใน C#: เก็บ​ข้อความ​ไว้ แต่​เสีย​ความ​สามารถ​ใน​การ​แยก​ประเภท จึง​เหมาะ​กับ main/app ที่​แค่​อยาก​รายงาน​แล้ว​จบ แต่​ไม่​เหมาะ​กับ​ลายเซ็น​สาธารณะ​ของ library ที่​ผู้​เรียก​ควร match กรณี​ต่างๆ ได้ — นั่น​คือ​เหตุผล​ที่​หัวข้อ​ถัด​ไป​เรา​สร้าง error enum เฉพาะ​เจาะจง​ด้วย thiserror แทน

สำหรับ library (code ที่​คน​อื่น​เรียก​ใช้) เรา​อยาก​ได้ error type ที่​ผู้​เรียก match แยก​กรณี​ได้​ครบ วิธี​เขียน impl Display และ impl Error ด้วย​มือ​นั้น​น่า​เบื่อ​และ​ซ้ำ thiserror เป็น derive macro ที่​สร้าง​ให้​ทั้งหมด ต่อ​ไป​นี้​คือ error type จริง​ของ taskcli:

use std::num::ParseIntError;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ConfigError {
#[error("could not read config file")]
Read(#[from] std::io::Error),
#[error("port `{0}` is not a valid number")]
BadPort(#[from] ParseIntError),
#[error("missing required key `{key}`")]
MissingKey { key: String },
}
pub fn load_port(path: &str) -> Result<u16, ConfigError> {
let text = std::fs::read_to_string(path)?; // io::Error -> ConfigError::Read
if text.trim().is_empty() {
return Err(ConfigError::MissingKey { key: "port".into() });
}
let port: u16 = text.trim().parse()?; // ParseIntError -> ConfigError::BadPort
Ok(port)
}

อ่านที​ละ​ชิ้น:

  • #[derive(Error, Debug)] — สร้าง impl Error ให้ และ ต้อง derive Debug คู่​เสมอ (trait Error บังคับ ไม่​งั้น compile ไม่​ผ่าน)
  • #[error("…")] — กำหนด​ข้อความ Display ของ​แต่ละ variant ใช้ {0} อ้าง field แบบ tuple และ {key} อ้าง field แบบ named ได้ตรงๆ
  • #[from] — สร้าง impl From<ต้นทาง> ให้​อัตโนมัติ และ ทำ field นั้น​เป็น source() ของ error นี่แหละ​คือ​สิ่ง​ที่​ทำให้ ? ใน​บรรทัด read_to_string(path)? แปลง io::Error เป็น ConfigError::Read ให้​เอง โดยที่​คุณ​ไม่​ต้อง​เขียน conversion เลย

สอง #[from] (io::Error กับ ParseIntError) อยู่​ร่วม​กัน​ได้ เพราะ​ชนิด​ต้นทาง​ต่าง​กัน — macro สร้าง impl From<io::Error> และ impl From<ParseIntError> แยก​กัน​คนละ​ตัว

สอง #[from] ชนิด​เดียวกัน = conflicting From impls

ถ้า2 variant ใช้ #[from] กับ​ชนิด​ต้นทาง เดียวกัน macro จะ​พยายาม​สร้าง impl From<T> ซ้ำ​สอง​ครั้ง → compiler ปฏิเสธ​ด้วย E0119 (conflicting implementations) เพราะ Rust ห้าม​มี impl From<T> ซ้ำ​สำหรับ​ชนิด​เดียวกัน วิธี​แก้​คือ​ให้ variant หนึ่ง​ใช้ #[from] และ​อีก variant แปลง​เอง หรือ​แยก​ชนิด​ต้นทาง

อีก​กับดัก: อย่า pin thiserror = "1" — major ปัจจุบัน​คือ 2.x code ที่​เห็น (#[derive(Error)], #[error], #[from]) หน้าตา​เหมือน​เดิม​เป๊ะ ต่าง​แค่​บรรทัด version (ของ​เก่า​ใน 1.x บาง​สำนวน​ถูก​ถอด​ออก​ใน 2.0 แล้ว)

พอ​มา​ถึง​ชั้น application (code main ของ taskcli เอง) คุณ​มัก​ไม่​อยาก​ประกาศ error enum ละเอียด​สำหรับ​ทุก​ความ​ผิดพลาด — คุณ​อยาก​แค่ ส่ง​ต่อ พร้อม​แนบ​บริบท​ว่า “ล้มเหลว​ตอน​กำลัง​ทำ​อะไร” แล้ว​รายงาน​ให้​ผู้​ใช้ anyhow เกิด​มา​เพื่อ​สิ่ง​นี้:

use anyhow::{bail, Context, Result};
use std::fs;
fn load_config(path: &str) -> Result<String> { // Result<T> == Result<T, anyhow::Error>
let contents = fs::read_to_string(path)
.with_context(|| format!("failed to read config from {path}"))?;
if contents.trim().is_empty() {
bail!("config file {path} is empty");
}
Ok(contents)
}
  • anyhow::Result<T> เป็น alias ของ Result<T, anyhow::Error>anyhow::Error ห่อ error ชนิด​ไหน​ก็ได้ (คล้าย Box<dyn Error> แต่​ทรง​พลัง​กว่า)
  • .with_context(|| …) แนบ​ข้อความ​อธิบาย​ชั้น​บน​เข้า​กับ error เดิม — เทียบ​ได้​กับ throw new Exception($"…: {inner}") ใน C# ที่​ยัง​เก็บ inner ไว้
  • bail!("…") = สร้าง error จาก​ข้อความ​แล้ว return ทันที (คู่​กับ ensure! ที่​เช็ก​เงื่อนไข​ก่อน)

หลักการ​ปิด​ท้าย​บท — จำ​ให้​ขึ้นใจ:

thiserror สำหรับ library, anyhow สำหรับ application (anyhow README, หัวข้อ “Comparison to thiserror”)

เหตุผล: library อยาก​ให้​ผู้​เรียก match แยก​กรณี​ได้ → ต้องการ enum เฉพาะ​เจาะจง (thiserror); application แค่​อยาก​ส่ง​ต่อ + รายงาน → อยาก​ได้​ความ​สะดวก (anyhow) และ​สอง​ตัว​นี้ ต่อ​กัน​ได้​อย่าง​ลงตัว: error ที่​สร้าง​ด้วย thiserror implement trait Error อยู่​แล้ว ดังนั้น​เมื่อ load_config เรียก function ที่​คืน ConfigError แล้ว​ใส่ ? ต่อ​ท้าย ? จะ​แปลง ConfigError เป็น anyhow::Error ให้​เอง​ผ่าน From ตัว​เดิม — library ผลิต error ที่​ละเอียด, application กลืน​มัน​ขึ้น​ชั้น​บน​ได้​ทันที

อย่า​อ้าง​เกิน​จริง​เรื่อง backtrace

anyhow เก็บ backtrace ให้ ก็​ต่อ​เมื่อ ตั้ง environment variable RUST_BACKTRACE เท่านั้น ไม่ใช่​จับ​อัตโนมัติ​ทุก​ครั้ง อย่า​เข้าใจ​ผิด​ว่า​ได้ backtrace ฟรี​เสมอเหมือน exception ของ .NET

Cargo.toml ของ​บท​นี้​เพิ่ม2 crate — จำหลัก caret req จาก​บท1: pin major แล้ว​ปล่อย​ให้ Cargo.lock ล็อก patch เอง:

[package]
name = "mycli"
version = "0.1.0"
edition = "2024"
[dependencies]
thiserror = "2"
anyhow = "1"
flowchart TB
  CALL["fs::read_to_string(path)?"]
  CALL --> Q{"ผลลัพธ์เป็น Ok หรือ Err?"}
  Q -->|"Ok(v)"| CONT["แกะค่า v ออกมา<br/>ทำงานต่อบรรทัดถัดไป"]
  Q -->|"Err(e)"| CONV["From::from(e)<br/>แปลง io::Error เป็น ConfigError"]
  CONV --> RET["return Err(ConfigError)<br/>early-return ออกจาก function ทันที"]

  classDef ok fill:#fde68a,stroke:#92400e,color:#451a03;
  classDef err fill:#fed7aa,stroke:#7c2d12,color:#431407;
  class CALL,CONT ok;
  class CONV,RET err;

คำ​บรรยาย​ภาพ: เส้นทาง​ของ​การ​เรียก​หนึ่ง​ครั้ง​ที่​ลงท้าย​ด้วย ? — ถ้า​ได้ Ok(v) จะ​แกะ​ค่า​แล้ว​เดิน​ต่อ; ถ้า​ได้ Err(e) ? จะ​เรียก From::from แปลง​ชนิด error (เช่น io::ErrorConfigError) แล้ว early-return ออก​จาก function ทันที การ​ส่ง​ต่อ​พร้อม​แปลง​ชนิด​นี้​ยุบ boilerplate การ​เขียน match-แล้ว-return ด้วย​มือ​ให้​เหลือ​อักขระ​เดียว

บท​นี้​วาง​ระบบ error ของ Rust ที่​ต่าง​จาก exception ของ C# ทั้ง​กระบวน: ความ​ผิดพลาด​ที่​กู้​ได้​เป็น ค่า Result<T,E> ที่ compiler บังคับ​ให้​รับรู้ (#[must_use]) ส่วน panic! สงวน​ไว้​ให้ bug ที่​กู้​ไม่​ได้​เท่านั้น; unwrap/expect แกะ​ค่า​แบบ panic-ถ้า​พลาด ใช้ได้​เฉพาะ​ใน test/prototype/จุด​ที่​พิสูจน์​ว่า​พลาด​ไม่​ได้; ตัว​ดำเนิน​การ ? ยุบ match-แล้ว-return ให้​เหลือ​อักขระ​เดียว พร้อม​แปลง​ชนิด error ด้วย From โดย​อัตโนมัติ (กลไก​ที่​บท6 จะ​เปิดเผย​ว่า​คือ trait From); fn main() -> Result<(), Box<dyn Error>> ให้ ? ส่ง​ต่อ error ต่าง​ชนิด​ใน binary ได้; thiserror 2.x สร้าง error enum ของ library แบบ matchable ด้วย #[derive(Error, Debug)] + #[error] + #[from]; และ​หลัก thiserror=library, anyhow=application ที่​ต่อ​กัน​ได้​เพราะ error ของ thiserror เป็น Error ที่ ? แปลง​เข้าไป​ใน anyhow::Error ได้

บท​หน้า​เรา​ขยับ​ไป​ที่ collections, strings, iterators และ closures — เรา​จะ​เห็น collect::<Result<Vec<_>, _>>() ที่ short-circuit หยุด​ทันที​ที่​เจอ Err ตัว​แรก ซึ่ง​เป็นการ​นำ Result ของ​บท​นี้​ไป​ประกอบ​ร่าง​กับ iterator ที่​ขี้เกียจ​ของ​บท​หน้า​อย่าง​สวยงาม


🔗 อ้างอิง​ต้นทาง​ของ​บท​นี้

บท​นี้​อิง​ต้นทาง​ที่​ลง​วัน​ที่​กำกับ อ่าน​ต่อ​ได้​โดยตรง:

  • The Rust Programming Language — ch09 Error Handling (เข้าถึง 2026-07-24) — การ​แบ่ง recoverable (Result) vs unrecoverable (panic!), unwrap/expect, และ ? ที่ early-return พร้อม​แปลง error ด้วย From
  • rustc Error Codes Index — E0119 (เข้าถึง 2026-07-24) — conflicting trait implementations (เหตุผล​ที่​สอง #[from] ชนิด​เดียวกัน​ไม่ compile)
  • thiserror v2.0.19 — docs.rs (as-of 2026-07-18) — #[derive(Error)], #[error("…")] = Display, #[from] = สร้าง From + ทำ field เป็น source
  • thiserror 2.0 release notes (2024) — major ปัจจุบัน​คือ 2.x (อย่า pin "1")
  • anyhow v1.0.104 — docs.rs (as-of 2026-07-18) — Result<T> alias, .with_context(), bail!, และ​หัวข้อ “Comparison to thiserror”: thiserror สำหรับ library, anyhow สำหรับ application

เช็กความเข้าใจ — บทที่ 4

ข้อ 1 / 3

ตัวดำเนินการ `?` ท้าย expression ที่คืน `Result` ทำอะไร?