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

Error Handling (เชิงลึก)

TypeScript errors ไม่ปรากฏที่ type level — ฟังก์ชันใดก็ได้สามารถ throw อะไรก็ได้ทุกเมื่อ และ compiler ไม่บังคับให้คุณจัดการ Rust เข้ารหัสความล้มเหลวโดยตรงใน return type: ฟังก์ชันที่อาจล้มเหลว return Result<T, E> โดยที่ T คือค่าสำเร็จและ E คือ error ถ้าคุณ ignore Result compiler จะเตือน

TypeScript
// TypeScript — errors are invisible in the signature
function parseId(s: string): number {
const n = parseInt(s, 10);
if (isNaN(n)) throw new Error(`Invalid id: ${s}`);
if (n === 0) throw new Error("id zero not allowed");
return n;
}
try {
console.log(parseId("42")); // 42
console.log(parseId("abc")); // throws
} catch (err) {
console.error(err instanceof Error ? err.message : err);
}
Rust
use std::num::ParseIntError;
#[derive(Debug)]
enum AppError {
Parse(ParseIntError),
NotFound(String),
}
impl std::fmt::Display for AppError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
AppError::Parse(e) => write!(f, "parse error: {e}"),
AppError::NotFound(s) => write!(f, "not found: {s}"),
}
}
}
impl From<ParseIntError> for AppError {
fn from(e: ParseIntError) -> Self { AppError::Parse(e) }
}
fn parse_id(s: &str) -> Result<u32, AppError> {
let n: u32 = s.trim().parse()?; // ? converts ParseIntError via From
if n == 0 {
return Err(AppError::NotFound(String::from("id zero not allowed")));
}
Ok(n)
}
fn main() {
println!("{:?}", parse_id("42"));
println!("{:?}", parse_id("abc"));
println!("{:?}", parse_id("0"));
}

? ที่ท้าย expression ที่ return Result คือ syntactic sugar ของ “ถ้าเป็น Err ให้แปลง error แล้ว return ออกไปเลย ไม่เช่นนั้นก็ unwrap ค่า Ok” เขียนตัวเดียวแทน match แบบเต็ม ๆ ได้หลายบล็อก

// ไม่มี ?
let n = match s.trim().parse::<u32>() {
Ok(val) => val,
Err(e) => return Err(AppError::from(e)),
};
// มี ? — behaviour เหมือนกันทุกประการ ตัวอักษรเดียว
let n: u32 = s.trim().parse()?;

สำหรับ ? ทำงานข้าม error types ต่างกัน target error type ต้อง implement From<SourceError> standard library และ thiserror จัดการนี้อัตโนมัติ

crate thiserror กำจัด boilerplate ของการเขียน Display และ From ด้วยมือ annotation #[derive(Error)] และ attribute #[error("...")] บนแต่ละ variant ก็เพียงพอ

TypeScript
// TypeScript — custom error class hierarchy
class AppError extends Error {
constructor(message: string) {
super(message);
this.name = "AppError";
}
}
class ParseError extends AppError {
constructor(public input: string) {
super(`Invalid input: ${input}`);
}
}
class NotFoundError extends AppError {
constructor(public id: string) {
super(`Not found: ${id}`);
}
}
Rust
use thiserror::Error;
#[derive(Error, Debug)]
enum AppError {
#[error("not found: {0}")]
NotFound(String),
#[error("parse error: {0}")]
Parse(#[from] std::num::ParseIntError),
}
fn parse_id(s: &str) -> Result<u32, AppError> {
let n: u32 = s.trim().parse()?; // ParseIntError -> AppError::Parse via #[from]
if n == 0 {
return Err(AppError::NotFound(String::from("id zero not allowed")));
}
Ok(n)
}
fn main() {
match parse_id("42") {
Ok(id) => println!("Parsed: {id}"),
Err(e) => println!("Error: {e}"),
}
match parse_id("abc") {
Ok(id) => println!("Parsed: {id}"),
Err(e) => println!("Error: {e}"),
}
match parse_id("0") {
Ok(id) => println!("Parsed: {id}"),
Err(e) => println!("Error: {e}"),
}
}

thiserror ใช้สำหรับ library code ที่ผู้เรียกต้องตรวจสอบ error type anyhow ใช้สำหรับ application/binary code ที่คุณแค่ต้องการให้ errors propagate อย่างชัดเจนพร้อม context messages ดีๆ โดยไม่ต้องการ match บน specific variants

use anyhow::{Context, Result};
fn read_config(path: &str) -> Result<String> {
std::fs::read_to_string(path)
.with_context(|| format!("failed to read config at {path}"))
}

anyhow::Result<T> เป็น alias ของ Result<T, anyhow::Error> error ใดก็ตามที่ implement std::error::Error สามารถแปลงเป็น anyhow::Error ได้ด้วย ?

use thiserror::Error;
#[derive(Error, Debug)]
enum AppError {
#[error("not found: {0}")]
NotFound(String),
#[error("parse error: {0}")]
Parse(#[from] std::num::ParseIntError),
}
fn parse_id(s: &str) -> Result<u32, AppError> {
let n: u32 = s.trim().parse()?;
if n == 0 {
return Err(AppError::NotFound(String::from("id zero not allowed")));
}
Ok(n)
}
fn main() {
match parse_id("42") {
Ok(id) => println!("Parsed: {id}"),
Err(e) => println!("Error: {e}"),
}
match parse_id("abc") {
Ok(id) => println!("Parsed: {id}"),
Err(e) => println!("Error: {e}"),
}
match parse_id("0") {
Ok(id) => println!("Parsed: {id}"),
Err(e) => println!("Error: {e}"),
}
}
`?` operator ทำอะไรเมื่อ apply กับ `Result::Err(e)`?
ควรใช้ `thiserror` vs `anyhow` เมื่อใด?
อะไรต้องเป็นจริงเพื่อให้ `?` แปลง error type `E1` เป็น `E2` อัตโนมัติ?
ความแตกต่างหลักระหว่าง `Result`-based error handling ของ Rust กับ try/catch ของ TypeScript คืออะไร?