วันหนึ่งผมค้นหาบทนำ Ruby exceptions สำหรับมือใหม่ — คนรู้ basic Ruby syntax แต่ไม่แน่ใจว่า exception คืออะไร ทำไมถึงมีประโยชน์ ไม่เจอ จึงลองเขียนเอง หวังว่าจะมีประโยชน์ หากจุดไหนสับสน tweet มาที่ @StarrHorne :)
Exception คืออะไร?
Exceptions เป็นวิธีของ Ruby จัดการ unexpected events
เคย typo ทำโปรแกรม crash พร้อมข้อความ SyntaxError หรือ NoMethodError? นั่นคือ exceptions ทำงาน
เมื่อ raise exception ใน Ruby โลกหยุดนิ่ง โปรแกรมเริ่ม shutdown หากไม่มีอะไรหยุด ก็จะ exit พร้อม error message
ตัวอย่าง: ลองหารด้วยศูนย์ ไม่ได้ Ruby raise exception ZeroDivisionError โปรแกรมหยุดพิมพ์ error
1 / 0
# Program crashes และ outputs: "ZeroDivisionError: divided by 0"
Crashing programs ทำ users โกรธ ปกติอยากหยุด shutdown process และตอบสนอง error อย่างฉลาด
เรียกว่า "rescuing", "handling", หรือ "catching" exception หมายความเหมือนกัน ทำใน Ruby แบบนี้:
begin
# Any exceptions in here...
1/0
rescue
# ...will cause this code to run
puts "Got an exception, but I'm responding intelligently!"
do_something_intelligent()
end
# โปรแกรมนี้ไม่ crash
# Outputs: "Got an exception, but I'm responding intelligently!"
Exception ยังเกิด แต่ไม่ crash เพราะ "rescued" แทนที่จะ exit Ruby รันโค้ดใน rescue block พิมพ์ข้อความ
ดีแต่มีข้อจำกัดใหญ่: บอกว่า "something went wrong" โดยไม่บอกว่า อะไร ผิด
ข้อมูลทั้งหมดเกี่ยวกับสิ่งที่ผิดอยู่ใน exception object
Exception Objects
Exception objects เป็น Ruby objects ธรรมดา เก็บข้อมูล "what happened" สำหรับ exception ที่ rescue
เพื่อรับ exception object ใช้ rescue syntax แบบต่างออกไป:
# Rescue all errors, ใส่ exception object ใน `e`
rescue => e
# Rescue เฉพาะ ZeroDivisionError ใส่ exception object ใน `e`
rescue ZeroDivisionError => e
ตัวอย่างที่สอง ZeroDivisionError เป็น class ของ object ใน e "types" ของ exceptions ที่พูดถึงคือ class names
Exception objects เก็บ debug data ที่มีประโยชน์ ดู exception object สำหรับ ZeroDivisionError
begin
1/0
rescue ZeroDivisionError => e
puts "Exception Class: #{ e.class.name }"
puts "Exception Message: #{ e.message }"
puts "Exception Backtrace: #{ e.backtrace }"
end
# Outputs:
# Exception Class: ZeroDivisionError
# Exception Message: divided by 0
# Exception Backtrace: ...backtrace as an array...
เหมือน Ruby exceptions หลายตัว มี message, backtrace และ class name
Raise Exceptions เอง
จนถึงตอนนี้ rescue exceptions แต่มัน trigger เองได้ เรียกว่า "raising" ทำโดยเรียกเมธอด raise
ตอน raise เอง เลือก exception type ได้ ตั้ง error message ได้
ตัวอย่าง:
begin
# raises ArgumentError with message "you messed up!"
raise ArgumentError.new("You messed up!")
rescue ArgumentError => e
puts e.message
end
# Outputs: You messed up!
สร้าง error object ใหม่ (ArgumentError) พร้อม custom message ส่งให้ raise
เป็น Ruby raise เรียกได้หลายแบบ:
# โปรดเพราะ explicit
raise RuntimeError.new("You messed up!")
# ...ผลลัพธ์เหมือนกัน
raise RuntimeError, "You messed up!"
# ...ผลลัพธ์เหมือนกัน แต่ raise RuntimeErrors ได้อย่างเดียว
raise "You messed up!"
สร้าง Custom Exceptions
Built-in exceptions ดี แต่ไม่ครอบคลุกทุก use case
ถ้าสร้าง user system อยาก raise exception ตอน user เข้าถึงส่วนห้าม ไม่มี standard exceptions ใดเข้ากัน สร้าง exception ใหม่ดีกว่า
สร้าง custom exception: สร้าง class ใหม่สืบทอดจาก StandardError
class PermissionDeniedError < StandardError
end
raise PermissionDeniedError.new()
แค่ Ruby class ปกติ เพิ่ม methods และ data ได้เหมือน class อื่น เพิ่ม attribute "action":
class PermissionDeniedError < StandardError
attr_reader :action
def initialize(message, action)
super(message)
@action = action
end
end
# ตอน user พยายามลบสิ่งที่ไม่มี permission
raise PermissionDeniedError.new("Permission Denied", :delete)
Class Hierarchy
เราสร้าง custom exception โดย subclass StandardError ซึ่งสืบทอดจาก Exception
จริงๆ class hierarchy ของ exception ใดๆ ใน Ruby สุดท้ายไปถึง Exception พิสูจน์ให้ดู: built-in exceptions ส่วนใหญ่ แสดงแบบ hierarchy
Exception
NoMemoryError
ScriptError
LoadError
NotImplementedError
SyntaxError
SignalException
Interrupt
StandardError
ArgumentError
IOError
EOFError
IndexError
LocalJumpError
NameError
NoMethodError
RangeError
FloatDomainError
RegexpError
RuntimeError
SecurityError
SystemCallError
SystemStackError
ThreadError
TypeError
ZeroDivisionError
SystemExit
ไม่ต้องจดทั้งหมด แสดงเพราะ hierarchy concept นี้สำคัญมากเพราะเหตุผลเฉพาะ
Rescue errors ของ class เฉพาะ จะ rescue errors ของ child classes ด้วย
ลองอีกครั้ง...
เวลา rescue StandardError ไม่แค่ rescue exceptions class StandardError แต่ children ด้วย ดู chart จะเยอะ: ArgumentError, IOError ฯลฯ
ถ้า rescue Exception จะ rescue ทุก exception ซึ่งเป็นไอเดียแย่มาก
Rescue ทุก Exceptions (วิธีผิด)
อยากโดนด่า ไป stack overflow โพสโค้ดแบบนี้:
// Don't do this
begin
do_something()
rescue Exception => e
...
end
โค้ดข้างต้น rescue ทุก exception อย่าทำ! จะทำให้โปรแกรมเสียแบบแปลก
เพราะ Ruby ใช้ exceptions ไม่ใช่แค่ errors ใช้จัดการ messages จาก OS ที่เรียก "Signals" เคยกด "ctrl-c" ออกโปรแกรม? ใช้ signal ด้วยการ suppress ทุก exceptions คุณกด suppress signals นั้นด้วย
ยังมี exceptions บางประเภท — เช่น syntax errors — ที่ควร crash โปรแกรม หาก suppress จะไม่รู้ตอน typo หรือผิดพลาดอื่นๆ
Rescue ทุก Errors (วิธีถูกต้อง)
กลับดู class hierarchy chart จะเห็น errors ทั้งหมดที่อยาก rescue เป็น children ของ StandardError
หมายถึงอยาก rescue "all errors" ควร rescue StandardError
begin
do_something()
rescue StandardError => e
# แค่ exceptions ของแอปคุณถูกกลืน SyntaxErrror เหลืออยู่
end
จริงๆ หากไม่ระบุ exception class Ruby สมมติว่าเป็น StandardError
begin
do_something()
rescue => e
# นี้เหมือน rescue StandardError
end
Rescue Specific Errors (วิธีที่ดีที่สุด)
ตอนรู้ rescue all errors ควรรู้ว่ามันมักเป็น bad idea, code smell, considered harmful ฯลฯ
มักบ่งชี้ว่า lazy ไม่ไปหาว่า exception ใดต้อง rescue และจะกลับมากวนตลอด
ใช้เวลาทำให้ถูกต้อง Rescue specific exceptions
begin
do_something()
rescue Errno::ETIMEDOUT => e
// Rescue เฉพาะ Errno::ETIMEDOUT exceptions
end
Rescue หลายชนิดใน rescue block เดียวได้ ไม่มีข้อคิดเห็น :)
begin
do_something()
rescue Errno::ETIMEDOUT, Errno::ECONNREFUSED => e
end