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

Replace Error Code with Exception

error code คือค่าคืนพิเศษ เช่น -1, null, false หรือ string ว่าง ที่ function ใช้บอกว่า “อันนี้ไม่สำเร็จ” ปัญหาคือหน้าตาเหมือนค่าปกติเป๊ะ caller จึงต้อง จำเอง ว่าต้องเช็ค ลืมครั้งเดียว sentinel ก็ไหลลงไปชั้นล่างราวกับเป็นข้อมูลจริง

Replace Error Code with Exception แยกสองช่องทางออกจากกัน ค่าคืนถือเฉพาะผลสำเร็จ; ความล้มเหลวเดินทางอีกเส้นที่ไม่อาจถูกเข้าใจผิดเงียบ ๆ ว่าเป็นความสำเร็จ ในภาษาที่มี exception (TypeScript, Python) คุณ throw/raise ใน Go และ Rust ไม่มี exception สำหรับความล้มเหลวธรรมดา — การย้ายที่เป็นสไตล์คือทำให้ error ชัดเจนใน type: คืน error ควบคู่กับค่าใน Go หรือ Result ใน Rust เราแสดงทั้งสอง และบอกว่าอันไหนเป็นอันไหน

function คืน -1 แปลว่า “ไม่พบ” คืน null แปลว่า “parse ไม่ได้” หรือคืน false แปลว่า “บันทึกไม่สำเร็จ” caller ทุกตัวจึงแบกภาระต้องเขียน check ที่ลืมได้ง่ายมาก และการลืมนั้นก็มองไม่เห็นจนกว่าค่าผิด ๆ จะไประเบิดที่ไหนสักแห่งไกล ๆ ที่แย่กว่านั้นคือ sentinel ตัวเดียวกันมักแปลคนละอย่างใน function คนละตัว เช่น -1 เป็น index ที่หาไม่เจอตรงนี้ แต่เป็นจำนวน error ตรงโน้น ผู้อ่านจึงมองแวบเดียวแล้วเชื่อไม่ได้ สรุปคือความล้มเหลวถูกลักลอบยัดเข้ามาในช่องทางของค่า ทั้งที่ไม่ควรอยู่ตรงนั้น

ตัวอย่างคือการถอนเงินจากบัญชี ตอนแรก function คืน -1 เมื่อเงินไม่พอ หลัง refactor จะส่งสัญญาณความล้มเหลวผ่านช่องทางของตัวเอง คือ exception ใน TS/Python และ error/Result แบบชัดเจนใน Go/Rust

// Before — caller must remember that -1 means failure
function withdraw(balance: number, amount: number): number {
if (amount > balance) {
return -1;
}
return balance - amount;
}
// After — failure cannot be confused with a balance
class InsufficientFundsError extends Error {}
function withdraw(balance: number, amount: number): number {
if (amount > balance) {
throw new InsufficientFundsError(`need ${amount}, have ${balance}`);
}
return balance - amount;
}
flowchart TD
  A["Function fails"] --> B{"How is failure reported?"}
  B -->|"Sentinel: -1 / null / false"| C["Caller must remember to check<br/>silent corruption if they forget"]
  B -->|"Exception (TS, Python)"| D["Failure cannot be ignored<br/>unwinds to a handler"]
  B -->|"error / Result (Go, Rust)"| E["Failure is explicit in the type<br/>compiler nudges you to handle it"]
ความล้มเหลวออกจากช่องทางของค่า — exception ใน TS/Python, error หรือ Result ใน Go/Rust
  1. ระบุว่า sentinel คือค่าไหนและแทนเงื่อนไขอะไร ปักหมุดให้ชัดว่ารายงานความล้มเหลวแบบใด
  2. เลือกช่องทางความล้มเหลวสำหรับภาษาของคุณ ใน TypeScript หรือ Python ให้นิยาม exception type เฉพาะ — class ที่มีชื่อดีกว่า Error/Exception เปล่า ๆ เพราะ caller catch ความล้มเหลว นี้ ได้โดยไม่กลืนความล้มเหลวที่ไม่เกี่ยวกันไปด้วย ใน Go ให้คืน error; ใน Rust ให้คืน Result ที่มี error แบบมี type
  3. ที่จุดความล้มเหลว แทนที่ return sentinel ด้วยสัญญาณใหม่: throw/raise หรือ return 0, err / return Err(...)
  4. อัปเดตเส้นทางสำเร็จให้คืนเฉพาะผลลัพธ์จริง — และใน Go/Rust ให้จับคู่กับ nil/Ok(...)
  5. ไล่แก้ caller ทุกตัว เปลี่ยน check แบบ “คืน sentinel มาหรือเปล่า” เป็นการจัดการที่เข้าคู่กัน คือ try/catch (หรือปล่อยให้ลอยขึ้นไป) สำหรับ exception ส่วน Go และ Rust ใช้ if err != nil หรือ ?/match
  6. รัน test หลังย้าย caller แต่ละราย เพื่อให้จุดที่พลาดโผล่ขึ้นมาทันทีแทนที่จะหลังจากเปลี่ยนทั้งหมดแล้ว

หยิบท่านี้มาใช้เมื่อ function รายงานความล้มเหลวด้วย sentinel ที่ปนอยู่ในช่องทางเดียวกับค่าปกติ และ sentinel นั้นมองข้ามง่ายหรือกำกวม พอย้ายความล้มเหลวขึ้นช่องทางของตัวเอง โอกาสจัดการพลาดก็ลดลงมาก exception ที่ไม่มีใครจับจะโผล่ออกมาดังลั่น ส่วน error/Result ที่ไม่ได้จัดการก็ยังมองเห็นได้ และใน Rust compiler ไม่ยอมให้คุณลืมเคส Err ด้วยซ้ำ

ข้อแลกเปลี่ยนต่างกันไปตามภาษา exception เหมาะกับเงื่อนไขที่ เป็นข้อยกเว้นจริง ๆ ไม่ใช่ผลลัพธ์ที่คาดว่าจะเจอเป็นกิจวัตร ถ้าเอามาใช้กับ control flow ปกติ code จะช้าและตามยาก ถ้าผลลัพธ์ “ไม่เจอ” เป็นเคสปกติที่คาดไว้อยู่แล้ว ให้เลือก type ว่างแบบชัดเจนแทน ดูเพิ่มที่ Introduce Special Case ส่วนใน Go และ Rust การใช้ error/Result แบบชัดเจนเป็นสไตล์ประจำอยู่แล้ว การ refactor ตรงนั้นจึงไม่ใช่ “เอา exception เข้ามา” แต่เป็น “เลิกเข้ารหัส error เป็นเลขวิเศษ” มากกว่า และไม่ว่าจะเลือกช่องทางไหน อย่าผสมสองแบบใน function เดียวกัน เพราะนั่นเพิ่มวิธีที่ caller จะทำพลาดเป็นสองเท่า

ทำไม error code (เช่นการคืน -1) จึงเสี่ยง?
สำหรับ Go และ Rust บทเรียนนำเสนออะไรเป็น "Replace Error Code" ที่เป็นสไตล์แทน exception?
เมื่อผลลัพธ์ "ที่หายไป" เป็นเคสปกติที่คาดไว้มากกว่าความล้มเหลวจริง บทเรียนแนะนำอะไรแทน?
ทำไมจึงเลือก exception type เฉพาะมากกว่า Error/Exception เปล่า ๆ ใน TypeScript หรือ Python?