[C#] Guidelines for Exception Handling

หลังจากเขียน Code ในการทำงานมาหลายปี พอดีเจอบทความนึงที่น่าสนใจ ผมเลยทำความเข้าใจ และสรุป เกี่ยวกับ 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 to your email.