Descriptive Error Messages
ข้อความ error ที่ช่วยทั้งผู้ใช้และนักพัฒนาให้เข้าใจปัญหา
คืออะไร
หัวข้อที่มีชื่อว่า “คืออะไร”เมื่อมีสิ่งไม่คาดคิดหรือผิดปกติเกิดขึ้นใน application มักมีข้อความ error แสดงต่อผู้ใช้ และ/หรือถูก log ไว้ที่ใดที่หนึ่งให้นักพัฒนาไปตรวจสอบ ข้อความเหล่านี้มีผู้อ่านสองกลุ่มที่ต้องการสิ่งต่างกัน:
- ผู้ใช้ปลายทาง ต้องการรู้ว่าเกิดอะไรขึ้น เกี่ยวข้องกับสิ่งที่เขาทำอยู่หรือไม่ และ (ถ้าเป็นไปได้) ควรทำอย่างไรต่อ
- นักพัฒนา/ทีม on-call ต้องการบริบทและรายละเอียดทาง technical ให้พอที่จะระบุต้นตอของปัญหาและแก้ไขได้โดยไม่ต้องเดา
หากเป็นข้อผิดพลาดชั่วคราว เช่นการขาดการเชื่อมต่อเครือข่าย หรือเป็นสิ่งที่ผู้ใช้อาจแก้ไขเองได้ด้วยการ troubleshoot ข้อความควรให้รายละเอียดพอที่ผู้ใช้จะเข้าใจและแก้ปัญหาได้ หากมุ่งให้ข้อมูล diagnostic แก่นักพัฒนา ก็ควรให้บริบทและ stack trace ที่ครบถ้วน ข้อความ error ที่ไม่ช่วยทั้งผู้ใช้และไม่มีประโยชน์ต่อนักพัฒนานั้นแย่ยิ่งกว่าไร้ประโยชน์ เพราะมันสร้างแต่ความหงุดหงิดโดยไม่ให้ทางออก — ข้อความอย่าง “An error occurred” หรือ “Something went wrong” คือตัวอย่างคลาสสิกของสิ่งที่ควรหลีกเลี่ยง
หลักการนี้ผูกกับ Fail Fast โดยตรง: การ fail เร็วมีประโยชน์ก็ต่อเมื่อข้อความที่ตามมาบอกได้ว่า ทำไม จึง fail ไม่เช่นนั้นก็เป็นเพียงการ crash เร็วขึ้นโดยไม่ช่วยวินิจฉัยอะไรเลย Nielsen Norman Group จัดให้การรายงาน error ที่ดีเป็นหนึ่งใน usability heuristic หลัก (heuristic #9: “Help Users Recognize, Diagnose, and Recover from Errors”) และสรุปไว้เป็นแนวทาง 12 ข้อสำหรับข้อความ error ที่ effective — เน้นความชัดเจน (explicit) อ่านง่าย (human-readable) สุภาพ (polite) แม่นยำ (precise) และให้คำแนะนำที่ทำตามได้ (constructive)
ทำอย่างไร
หัวข้อที่มีชื่อว่า “ทำอย่างไร”ออกแบบข้อความสำหรับผู้ใช้ก่อน
หัวข้อที่มีชื่อว่า “ออกแบบข้อความสำหรับผู้ใช้ก่อน”ข้อความ error ที่ผู้ใช้เห็นเป็นส่วนหนึ่งของ UI และ UX เช่นเดียวกับปุ่มหรือฟอร์ม — ถ้าคุณใส่ใจออกแบบ UX มาทั้งระบบ อย่าปล่อยให้ทุกอย่างพังทลายตอนเกิด error ด้วยการโชว์ stack trace หรือข้อความที่เขียนมาเพื่อ debugger ไม่ใช่มนุษย์ ตาม NNGroup ข้อความ error ที่ดีควรจะ:
- เจาะจง (specific) — บอกว่าอะไรผิดพลาดจริง ๆ แทนคำกลาง ๆ เช่น “เกิดข้อผิดพลาด”
- อ่านง่าย (human-readable) — ใช้ภาษาที่คนทั่วไปเข้าใจ ซ่อนรหัส error ที่ไม่มีความหมายไว้เบื้องหลัง (แต่ยังเก็บไว้สำหรับ support)
- สุภาพและไม่ตำหนิผู้ใช้ (polite) — เลี่ยงคำว่า “invalid” หรือ “คุณทำผิด”; ระบบควรปรับตัวเข้าหาผู้ใช้ ไม่ใช่โยนความผิดให้เขา
- ให้คำแนะนำที่ทำตามได้ (constructive) — ไม่ใช่แค่บอกปัญหา แต่บอกวิธีแก้หรือขั้นตอนถัดไปด้วย เช่นแทน “สินค้าหมด” ด้วย “สินค้าหมดชั่วคราว จะกลับมาวันที่ … หรือลงทะเบียนรับแจ้งเตือนได้ที่นี่”
แนวปฏิบัติเสริม: วางข้อความไว้ใกล้จุดที่เกิดปัญหา ใช้สัญลักษณ์ทางสายตา (ตัวหนา สีตัดกัน) ประกอบข้อความ และรักษาข้อมูลที่ผู้ใช้กรอกไว้ไม่ให้หายเมื่อเกิด error (ให้แก้ไขต่อได้ ไม่ต้องเริ่มใหม่)
แยกข้อความผู้ใช้ออกจากรายละเอียด diagnostic
หัวข้อที่มีชื่อว่า “แยกข้อความผู้ใช้ออกจากรายละเอียด diagnostic”อย่าให้ stack trace, connection string หรือ query ดิบรั่วไหลไปถึงผู้ใช้ — นอกจากจะน่ากลัวและอ่านไม่รู้เรื่องแล้วยังเป็นความเสี่ยงด้าน security ทางที่ดีคือ log รายละเอียดเต็มไว้ฝั่ง server พร้อม correlation id หรือ trace id แล้วแสดงข้อความสั้น ๆ ที่เป็นมิตรพร้อม id นั้นให้ผู้ใช้ เพื่อให้ทีม support อ้างอิงกลับไปหา log ได้ — แนวทางนี้เชื่อมกับการทำ Observability ในระบบ
flowchart TD
A[Exception เกิดขึ้น] --> B{รู้จักชนิด error หรือไม่}
B -->|รู้จัก domain exception| C[แปลงเป็นข้อความที่ผู้ใช้เข้าใจ]
B -->|ไม่รู้จัก generic exception| D[Log เต็มพร้อม correlation id]
D --> E[แสดงข้อความกลางพร้อม id อ้างอิง]
C --> F[แสดงพร้อมคำแนะนำขั้นตอนถัดไป]
D --> G[แจ้งทีม on-call ถ้าจำเป็น]
เคล็ดลับการจัดการ exception ฝั่ง code
หัวข้อที่มีชื่อว่า “เคล็ดลับการจัดการ exception ฝั่ง code”- อย่า catch โดยไม่ throw ต่อ (เว้นแต่คุณแก้ปัญหาได้ทันที)
- อย่า catch แล้ว throw Exception ตัวใหม่โดยไม่ห่อ exception เดิมไว้เป็น inner exception — ถ้าจะ log แล้ว re-throw เฉย ๆ ให้ใช้
throw;ไม่ใช่throw ex; - เลี่ยงการ catch generic exception (เช่น
System.Exception) ยกเว้นที่ระดับสูงสุดของ app (global exception handler) - สร้าง domain-specific exception (เช่น
InventoryUnavailableException) แทนกรณีที่รู้จัก เพื่อสื่อความหมายในระดับ abstraction ที่สูงขึ้น และ catch แยกจาก exception ทั่วไปได้ - ตาม Microsoft Learn: ตั้งชื่อ custom exception ให้ลงท้ายด้วย
Exception, มี constructor อย่างน้อยสามแบบ (ไม่มี parameter, รับ message, รับ message กับ inner exception), เขียน message เป็นประโยคสมบูรณ์ที่มีเครื่องหมายจบประโยค - เลี่ยงการใช้ exception เป็นตรรกะการไหลปกติของ app (เช่นเช็กว่า file มีอยู่ก่อนเขียน แทนที่จะเขียนแล้วค่อยดัก exception) — ใช้ method แบบ
TryParse/TryGetValueเมื่อ error เป็นกรณีปกติที่คาดว่าจะเกิด - เวลา log ให้ log
Exception.ToString()(หรือทั้ง Exception หาก log framework รับได้) ไม่ใช่แค่Exception.MessageเพราะToString()มี stack trace และ inner exception ครบ
กรณีมี error หลายจุดพร้อมกัน (validation)
หัวข้อที่มีชื่อว่า “กรณีมี error หลายจุดพร้อมกัน (validation)”เมื่อ validate ข้อมูลนำเข้า (เช่นฟอร์ม) การ throw exception ทันทีที่เจอ error แรกจะบังคับให้ผู้ใช้แก้ทีละจุดแบบ “ตีตัวตุ่น” Martin Fowler เสนอ Notification pattern: แทนที่จะ throw ทันที ให้สะสม error ทั้งหมดไว้ใน object notification แล้วรายงานให้ผู้ใช้เห็นครบในครั้งเดียว ซึ่งเหมาะกับ error ที่เป็น “พฤติกรรมที่คาดไว้แล้ว” (เช่นผู้ใช้กรอกฟอร์มผิด) มากกว่าการใช้ exception ซึ่งควรสงวนไว้กับสิ่งที่อยู่นอกเหนือความคาดหมายจริง ๆ
ตัวอย่าง
หัวข้อที่มีชื่อว่า “ตัวอย่าง”อย่าทำ — throw ex; รีเซ็ต stack trace ต้นตอหาย
catch (Exception ex){ _log.Error(ex); throw ex; // stack trace เดิมหาย! debug ยากขึ้นมาก}ทำแบบนี้ — throw; คง stack trace เดิมไว้ครบ
catch (Exception ex){ _log.Error(ex.ToString()); throw; // re-throw โดยรักษา stack trace ต้นตอไว้ครบ}Domain-specific exception ตามแนวทาง Microsoft Learn — 3 constructor มาตรฐาน
public class InventoryUnavailableException : Exception{ public string Sku { get; }
public InventoryUnavailableException() { }
public InventoryUnavailableException(string message) : base(message) { }
public InventoryUnavailableException(string message, Exception innerException) : base(message, innerException) { }
public InventoryUnavailableException(string sku, string message, Exception innerException) : base(message, innerException) { Sku = sku; }}
// ใช้งาน: ข้อความเจาะจง อ่านง่าย และให้ context (sku) สำหรับ diagnosticif (!inventory.HasStock(sku)){ throw new InventoryUnavailableException( sku, $"สินค้ารหัส {sku} หมดสต็อกชั่วคราว ไม่สามารถจองออเดอร์ได้ในขณะนี้", innerException: null);}แปลง exception ภายในให้เป็นข้อความที่ผู้ใช้เข้าใจ พร้อม correlation id สำหรับ support
try{ orderService.PlaceOrder(order);}catch (InventoryUnavailableException ex){ // รู้จักชนิดปัญหา — แสดงข้อความที่นำไปทำอะไรต่อได้ return Results.Conflict(new { message = ex.Message, sku = ex.Sku });}catch (Exception ex){ // ไม่รู้จักชนิดปัญหา — log เต็มพร้อม correlation id แล้วส่งข้อความกลางกลับ var correlationId = Guid.NewGuid(); _log.Error($"[{correlationId}] {ex}"); return Results.Problem( title: "ไม่สามารถทำรายการได้ในขณะนี้", detail: $"กรุณาลองใหม่อีกครั้ง หากยังพบปัญหาโปรดแจ้งรหัสอ้างอิง {correlationId}");}ประโยชน์และข้อควรระวัง
หัวข้อที่มีชื่อว่า “ประโยชน์และข้อควรระวัง”ประโยชน์
- ผู้ใช้แก้ปัญหาเองได้บ่อยขึ้นโดยไม่ต้องติดต่อ support ลดภาระทีม
- นักพัฒนาวินิจฉัยต้นตอปัญหาได้เร็วขึ้นเมื่อเกิด incident เพราะมี context และ stack trace ครบ ช่วยลด MTTR (mean time to resolution)
- ข้อความที่สุภาพและมีคำแนะนำช่วยรักษาความไว้ใจของผู้ใช้ต่อผลิตภัณฑ์ แม้ในสถานการณ์ที่แย่ที่สุด
- แยกข้อความผู้ใช้ออกจาก log diagnostic ทำให้ปรับปรุงแต่ละฝั่งได้อิสระ (เช่น localize ข้อความผู้ใช้ โดยไม่ต้องแตะ log format)
ข้อควรระวัง
- อย่าปล่อยให้รายละเอียดภายใน (stack trace, connection string, query, path บน server) รั่วไหลไปถึงผู้ใช้ ทั้งเรื่อง UX ที่แย่และความเสี่ยงด้าน security
- ข้อความที่ยาวหรือ technical เกินไปสำหรับผู้ใช้ทั่วไปสร้างความเหนื่อยล้า (alert fatigue) เช่นเดียวกับข้อความที่สั้นเกินจนไม่มีประโยชน์
- การ catch generic exception มากเกินไปเพื่อ “ครอบคลุมทุกกรณี” มักซ่อน bug จริงไว้ และทำให้ debug ยากขึ้นในระยะยาว — ใช้ domain-specific exception แทน
- ถ้าระบบ log ต้อง localize ข้อความผู้ใช้หลายภาษา ต้องแยก resource string ออกจาก log message ตั้งแต่ต้น ไม่เช่นนั้นจะปนกันจนดูแลยาก
- การใช้ exception แทน control flow ปกติ (เช่นแทน
TryParse) มีต้นทุนด้าน performance ที่ไม่คุ้มเมื่อ error เป็นกรณีที่คาดว่าจะเกิดบ่อย
ที่เกี่ยวข้อง
หัวข้อที่มีชื่อว่า “ที่เกี่ยวข้อง”- Fail Fast
- Defensive Programming
- Least Astonishment
- Observability
- Robustness Principle (Postel’s Law)