Rust Result.inspect_err: สังเกต error โดยไม่ทำลายเส้นทางส่งต่อ
เวลาเพิ่ม log ใน chain ของ Result เรามักเผลอใช้ map_err ทั้งที่ไม่ได้ต้องการแปลง error ผลคือ closure ต้องคืนค่าเดิมและผู้อ่านต้องตรวจเพิ่มว่ามี semantic change หรือไม่ Result::inspect_err บอก intent ตรงกว่า: ยืม error มาให้สังเกต แล้วคืน Result เดิมต่อไปครับ
มันเป็นคู่กับ inspect ฝั่งสำเร็จ closure รับ &E ไม่ได้ยึด ownership จึงเหมาะกับ counter, tracing field หรือ log ที่ไม่เปลี่ยนผลลัพธ์ หลังจากนั้นยังใช้ ?, map_err หรือ or_else ตามนโยบายจริงได้
use std::num::ParseIntError;
fn parse_port(raw: &str) -> Result<u16, ParseIntError> {
raw.parse::<u16>()
.inspect_err(|error| eprintln!("invalid port: {error}"))
}
fn main() {
assert_eq!(parse_port("8080"), Ok(8080));
let failed = parse_port("eight");
assert!(failed.is_err());
let message = failed
.map_err(|error| format!("configuration error: {error}"))
.unwrap_err();
assert!(message.starts_with("configuration error:"));
}
เส้นแบ่งที่ควรรักษา
inspect_err ไม่ใช่ recovery ถ้าต้องใส่ค่า fallback ให้ใช้ unwrap_or_else หรือ or_else; ถ้าต้องเพิ่ม context หรือเปลี่ยน error type ให้ใช้ map_err การแยกสามหน้าที่นี้ทำให้ chain อ่านจากซ้ายไปขวาได้ว่า “สังเกต → แปลง → ส่งต่อ” โดยไม่ซ่อนการเปลี่ยนพฤติกรรมไว้ใน callback
อย่า log ทุกชั้นของ call stack โดยอัตโนมัติ เพราะ error เดียวอาจถูกพิมพ์ซ้ำหลายครั้งและเพิ่มข้อมูลสำคัญลง log โดยไม่ตั้งใจ เลือก boundary ที่รู้บริบท เช่น ตอนรับ config หรือก่อนตอบ request และหลีกเลี่ยง raw payload, token หรือ path ส่วนตัว ใช้ structured fields ที่จำเป็นต่อการหาเหตุแทนข้อความทั้งก้อน
อีกจุดคือ side effect ใน inspect_err ควรเบาและไม่ panic หาก exporter ล้มแล้ว closure panic เราจะเสีย error ต้นฉบับที่กำลังวินิจฉัย สำหรับ metrics แบบ best-effort ให้ callback ไม่เปลี่ยน control flow และเขียนเทสต์ยืนยันว่า error type/value เดิมยังผ่านไปถึง caller
Recap
- ใช้
inspect_errเพื่อ observation โดยไม่เปลี่ยนResult - ใช้
map_errเมื่อต้องแปลงชนิดหรือเพิ่ม context - log ที่ boundary เดียวและกรองข้อมูลลับ
- อย่าให้ telemetry failure กลบ error ต้นฉบับครับ