[C#] Guidelines for Exception Handling

หลังจากเขียน Code ในการทำงานมาหลายปี พอดีเจอบทความนึงที่น่าสนใจ ผมเลยทำความเข้าใจ และสรุป เกี่ยวกับ Exception ดังนี้ครับ

Exception คือ อะไร ?

Exception คือ การแจ้งข้อมูลข้อผิดพลาดที่เกิดขึ้น จากการทำงานของระบบครับ มันมีกลุ่มที่เราสามารถจัดการเองได้ ใช้ Try Catch เข้าช่วย หรือจัดการไม่ได้เลยพวกกลุ่ม Error

Guidelines for Exception Handling

- Catch only the exceptions that you can handle.

เวลาเขียน Code ใน Method หรือ Class ให้จัดการ Exception ที่น่าจะเกิดขึ้นได้กับชิ้นงานที่ทำอยู่ และพยายามแจ้งกับ User ดูภาษาที่เข้าใจง่าย ส่วน Exception นอกเหนือจากนี้ ปล่อยให้ Caller เป็นคนจัดการ เข่น ถ้าทำ Component ในการอ่านไฟล์ สิ่งที่เราสนใจ

  • ทำ Component อ่านไฟล์ กลุ่ม Exception ควรเป็นกลุ่ม IO
  • ถ้าเกิด Expception จริง เช่น ไฟล์มันถูกอ่านอยู่ หลังจาก Exception เกิดขึ้น สิ่งที่เราควรทำ คือ แจ้ง Message ที่ User สามารถเข้าใจง่าย และเลือกที่ทำอะไรต่อไป
  • ไม่จำเป็นต้องดัก Exception แปลก เช่น StackOverflowException ถ้าเกิดขึ้นจริง สิ่งที่ทำได้ คือ Throw(โยน) ไปให้คนที่มีหน้าที่จริงๆจัดการ
- Don’t hide (bury) exceptions you don’t fully handle.

ใช้ Catch เท่าที่จำเป็น บางเรื่องเราไม่จำเป็นต้องรับมาหมด โยน(Throw) ให้คนที่เกี่ยวข้องจัดการดีกว่า อย่าซ่อน Exception เพราะ คิดว่า User จะตกใจ เช่น

  • ทำ Process B เกี่ยวกับคำนวณอยู่ Exception ที่เราต้องใช้น่าจะมี ArithmeticException, DividebyZeroException  เป็นต้น
  • เมื่อตอน Runtime มันเกิด Expception OutofMemory ขึ้นมา
  • สิ่งที่เราควรทำ คือ โยนให้ Process A ที่เรียก B มาทำงาน จัดการ Exception อย่าซ่อนมันไว้ (ถ้าซ่อนไว้ เราอาจจะเจอเคสที่อยู่ App ปิดตัวเองแบบเงียบๆ ไม่แจ้งอะไร)
- Use System.Exception and general catch blocks rarely.
  • พยายามอย่าดักจับ Class Exception ตรงๆ ใช้ให้น้อยที่สุด อาจจะใช้ในกรณีที่ Exception ที่ถูกโยนเข้ามามันไม่มีทางไปแล้ว เพื่อให้ Exception แสดงออกมา
  • Catch Exception จากเล็กไปใหญ่เสมอ ตาม Code ดังนี้
try
{
    //Do some business, logic here !!!
}
catch (DivideByZeroException ex) 
{
    Console.WriteLine("Exception Divide by Zero occur:" + ex);
}
catch (ArithmeticException ex) 
{
    Console.WriteLine("Exception Arithmeti cException  occur:" + ex);
}
catch (Exception)
{
    //I Cannot handle. someone help please.
    throw;
}

จาก Code นี้ ผมเรียก Exception ดังนี้

  • ด่านแรก DividebyZeroException เพราะถูก Extend มาจาก ArithmeticException
  • ด่านสอง ArithmeticException เพราะถูก Extend มาจาก SystemException
  • ด่านสุดท้าย Exception - Mother of Exception เป็น Class แม่ของเหล่า Exception เลย - ใช้เท่าที่จำเป็น เพราะ ปกติแล้ว เวลาที่เราเขียน Code ตัว IDE มันจะ Hint Exception มาให้ หรือ ถ้าถึกหน่อยไปดูใน MSDN
- Use throw; rather than throw <exception object> inside a catch block.
  • มารู้จักกับ throw และ throw <exception object> กันก่อน
  • throw <exception object> มาดู Code กันเลย
try 
{ 
    //Do some business, logic here !!!
} 
catch (Exception ex) 
{
    //I Cannot handle. someone help please.
    throw ex; 
}
  • อันนี้ คือการ Throw Exception เปลี่ยน call stack  ของโปรแกรม โดย call stack จะมาสุด ณ ตำแหน่งที่เรา throw exception ออกมา
  • ซึ่งเมื่อเกิด exception ขึ้นจะทำให้เรา trace ปัญหาที่เกิดได้ยากขึ้น เพราะไม่รู้ว่าเกิดขึ้นที่ตรงไหน
  • throw หรือ Rethrow มาดู Code กันเลย
try 
{ 
    //Do some business, logic here !!! 
} 
catch (Exception) 
{
    //I Cannot handle. someone help please. 
    throw; 
}
  • การ Rethrow exception ออกมาโดยที่ไม่ได้เปลี่ยน call stack ดังนั้นถ้าเราต้องการที่จะ throw exception เดิมออกมา ให้ใช้ throw
  • ตัวอย่าง Code ของ Throw vs Rethrow ได้ที่ Git ครับ
  • ใช้คำสั่ง throw; สำหรับ Exception ในกลุ่มเดียวกัน เพื่อรักษา call stack เดิมไว้ ส่วนถ้า Exception คนละชนิดให้ดูที่ข้อสุดท้าย
- Avoid exception reporting or logging lower in the call stack.

ต้องย้อนกลับไปข้อที่แล้วก่อน ระหว่าง throw(Rethrow) กับ throw ex

  • throw ex - Call Stack เปลี่ยน
  • rethrow - Call Stack ไม่เปลี่ยน

หลังจากเข้าใจ throw กับ rethrow แล้วมาดูการเขียน Log หรือ Report ให้ User ทราบกันดีกว่า ว่าควรทำอย่างไร

  • แบบ rethrow - การเขียน Log หรือ Report ควรจะเขียนใน High Stack (ไปเขียนเอาตอนสุดท้ายสุดเลย ไม่ต้อง Log ตอน rethrow) เพื่อป้องกันการเขียน Log ซ้ำซ้อน
  • แบบ throw - ต้องดูลำดับของ Call Stack ด้วย ถ้าเขียน Log ไม่ดีข้อมูลหายไปนะครับ

System.Console.WriteLine() ไม่ต้องพ่นออกมานะ เพราะใน WebApp กับ WinApp พ่นไป ก็ไม่มีประโยชน์

- Avoid exception conditionals that might change over time.

Avoid exception conditionals that might change over time.

  • อย่าเอา Exception Message มาเป็นเงื่อนไข หรือ ใช้ exception conditionals ของ C#6 ถ้าไม่มั่นใจว่า Business Logic มันจะนิ่ง หรือ Message นิ่ง ให้ระวังเรื่องภาษาด้วย เพราะ Lib บางตัว อาจจะมีการทำ custom exception แยกตาม แต่ละภาษาทำให้ดักมากกว่าเดิมอีก
  • *สำหรับ exception conditionals - ส่วนตัวมองว่า มันควรใช้ในตอนท้ายสุด ใช้ในการดักจับ Error Code หรือ Message เพื่อแจ้งคำอธิบาย หรือ การแก้ไขเบื้องต้นให้ User มากกว่า
- Avoid throwing exceptions from exception conditionals.

ต่อจากข้อที่แล้วครับ ถ้าเราใช้ exception conditionals มากรอง Exception แล้วโยนต่อไป ปัญหา คือ ฝั่งที่รับ Exception มาจัดการ จะไม่รู้ว่ามันมีสาเหตุมาอย่างไร และควรจัดการอย่างไรครับ

- Use caution when rethrowing different exceptions.

ระมัดระวังในการ rethrowing สำหรับ Exception ที่แตกต่างกัน การไปปรับเปลี่ยนชนิดของ Exception มันทำให้ Call Stack หายไปครับ ใน .Net มีวิธีการแก้ปัญหามาแล้ว คือ การใช้งาน Inner Exception เอาไว้เก็บที่มาของ Exception ล่าสุดที่เกิดขึ้นครับ

ตัวอย่างของ Inner Exception ลองดูตามตัวอย่างผมนะครับ อันแรก

  • เรากำลังเขียนไฟล์อยู่ พบว่าไฟล์ไม่มี เลย Throw FileNotFoundException
  • สิ่งที่ทำให้เกิด FileNotFoundException มาจาก Argument ที่ส่งเข้ามาผิด (จาก Code ไม่ส่งมาเลย และไฟล์ไม่มีอยู่จริงด้วย)
  • สิ่งที่ระบบสร้าง FileNotFoundException โดยมี ArgumentException เป็น Inner Exception เพื่อมาบอกสาเหตุที่แท้จริงครับ
  • ลองดู Code กันเลยย
static void Main(string[] args)
{
    try
    {
        ReadSomething();
    }
    catch (Exception e)
    {
        Console.WriteLine(String.Concat(e.StackTrace, e.Message));

        if (e.InnerException != null)
        {
            Console.WriteLine("Inner Exception");
            Console.WriteLine(String.Concat(e.InnerException.StackTrace, e.InnerException.Message));
        }
    }
    Console.ReadLine();
}

static void ReadSomething()
{
    try
    {
        throw new ArgumentException();
    }
    catch (ArgumentException e)
    {
        //make sure this path does not exist
        if (File.Exists("test.txt") == false)
        {
            throw new FileNotFoundException("File Not found when trying to write argument exception to the file", e);
        }
    }
}

มาตัวอย่างที่สอง

  • A ทำการ Process คิดเลขสักอย่าง A มีแต่การคำนวณ ดัก Exception arithmeticexception, DividebyZeroException
  • A เรียก B มาประมวลผล B ไปอ่าน TextFile อ้าวคนอื่นใช้อยู่ Throw Exception มา IOException
  • ใช้ InnerException สิ โดยเป็น A - arithmeticexception และมี ฺB - IOException เป็น inner exception
** สำหรับสถานการณ์อื่นๆ ลองอ่าน Guideline แล้วลองไปปรับดูครับ
  • Changing the exception type clarifies the problem - ปรับชนิดของ Exception ให้เหมาะสม เพื่อให้สามารถวิเคราะห์ปัญหาได้ง่ายขึ้นครับ
  • Private data is part of the original exception - ข้อมูล Exception บางอย่าง อาจจะมีข้อมูลที่สำคัญกับระบบ เราควรห่อหุ่มมันให้เรียบร้อย
  • The exception type is too specific for the caller to handle appropriately - บาง Exception มันเหมาะสมที่จะโยนขึ้นไปอยู่แล้ว เช่น Error Code ของ Database มันมีความชัดเจนอยู่ในตัว อย่าทำให้มันหายไปครับ เก็บไว้ใน inner exception ครับ

สำหรับผมแล้วบทความนี้สามารถนำไปปรับใช้ได้กับหลายๆภาษาครับ แต่อาจจะต้องเข้าใจ พื้นฐานของแต่ละภาษาก่อนครับ

Reference


Discover more from naiwaen@DebuggingSoft

Subscribe to get the latest posts sent to your email.