ที่ AppSignal เราให้ error tracking สำหรับ Ruby applications เพื่อทำเช่นนั้น เราจับ exceptions ทั้งหมดที่ applications โยนมา และแจ้ง developers เมื่อเกิดขึ้น
Exception handling อาจทำให้ถูกต้องยาก ใน article นี้เราจะอธิบายว่ามันทำงานยังไง ปัญหาที่ bad handling ก่อให้เกิด และ rescue exceptions อย่างถูกต้องอย่างไร
Rescuing Exceptions
ด้วยการ rescue exceptions ใน Ruby คุณป้องกัน application ไม่ crash ทันทีเมื่อสิ่งผิดพลาด ด้วย begin .. rescue block คุณระบุ alternative path สำหรับ application เมื่อ error เกิด
begin
File.read "config.yml"
rescue
puts "No config file found. Using defaults."
end
เป็นไปได้ที่ระบุว่าควร rescue exceptions ไหน เมื่อระบุ exception class subclasses ทั้งหมดของ exception นี้ก็จับได้
begin
File.read "config.yml"
rescue SystemCallError => e
puts e.class # => Errno::ENOENT
puts e.class.superclass # => SystemCallError
puts e.class.superclass.superclass # => StandardError
end
ในตัวอย่างข้างต้น คุณเห็น exception Errno::ENOENT ถูกจับเมื่อ parent SystemCallError ถูก rescue
Rescuing Too High Up in the Exception Chain
สำคัญคืออย่า rescue exceptions สูงเกินไปใน Exception chain เมื่อทำเช่นนั้น all subclassed exceptions จะถูกจับ ทำให้ rescue block's capture generic เกินไป
นี่คือโปรแกรมที่อ่าน config file ตาม argument ที่ส่งไปโปรแกรม
# $ ruby example.rb config.yml
def config_file
ARGV.firs # Note the typo here, we meant `ARGV.first`.
end
begin
File.read config_file
rescue
puts "Couldn't read the config file"
end
Error message บอกว่าไม่ได้อ่าน config file แต่ปัญหาจริงคือ typo ในโค้ด
begin
File.read config_file
rescue => e
puts e.inspect
end
#
Default exception class ที่ begin .. rescue block จับคือ StandardError หากเราไม่ส่ง specific class Ruby จะ rescue StandardError และ subclassed errors ทั้งหมด NoMethodError เป็นหนึ่งใน errors นี้
Rescue specific exception class จะช่วยป้องกัน unrelated errors accidentally prompt failure state มันยัง allow specific custom error messages ที่ helpful สำหรับ end user
config_file = "config.yml"
begin
File.read config_file
rescue Errno::ENOENT => e
puts "File or directory #{config_file} doesn't exist."
rescue Errno::EACCES => e
puts "Can't read from #{config_file}. No permission."
end
Rescuing Exception
มันยัง tempting ที่จะ rescue สูงใน exception chain Rescue ทุก errors ที่ application raise จะป้องกัน crash (100% uptime here we come!) แต่ก่อให้เกิดปัญหามาก
Exception class เป็น main exception class ใน Ruby All other exceptions เป็น subclasses ของ class นี้ หาก Exception rescued all errors จะถูกจับ
Two exceptions ที่ most applications ไม่ต้องการ rescue คือ SignalException และ SystemExit
SignalException ใช้เมื่อ outside source บอก application หยุด อาจเป็น Operating System ตอน shutdown หรือ system administrator ที่อยากหยุด application
SystemExit ใช้เมื่อ
exitถูกเรียกจาก Ruby application เมื่อ raised developer อยากให้ application หยุด
หากเรา rescue Exception และ exceptions เหล่านี้ raised ขณะ application กำลังรัน
begin ... rescue ... endblock มันไม่สามารถ exit ได้
โดยทั่วไป rescue Exception ใน normal situations เป็น bad idea เมื่อ rescue Exception คุณป้องกัน SignalException และ SystemExit ไม่ทำงาน แต่ยังมี LoadError, SyntaxError และ NoMemoryError เพื่อนำไปสู่บ้าง มันดีกว่า rescue specific exceptions แทน
Failures ใน Tests
เมื่อ Exception rescued ด้วย rescue Exception => e สิ่งอื่นนอกจาก application อาจแตก Test suite จริงๆ อาจซ่อนบาง errors
ใน minitest และ RSpec assertions ที่ fail จะ raise exception เพื่อแจ้ง failed assertion fail test เมื่อพวกมันทำ พวกมัน raise custom exceptions ของพวกเขา subclassed from Exception
หาก Exception rescued ใน test หรือ application code อาจ silence assertion failure
# RSpec example
def foo(bar)
bar.baz
rescue Exception => e
puts "This test should actually fail"
# Failure/Error: bar.baz
# received unexpected message :baz with (no args)
end
describe "#foo" do
it "hides an 'unexpected message' exception" do
bar = double(to_s: "")
foo(bar)
end
end
Expecting Exceptions
บางโค้ดหมายที่จะ raise exceptions ใน test suite เป็นไปได้ที่ silence exception เพื่อ test ไม่ fail เมื่อ raised
def foo
raise RuntimeError, "something went wrong"
end
foo
rescue RuntimeError
อย่างไรก็ตาม นี่ไม่ test ถ้า exception raised หรือไม่ เมื่อ exception ไม่ raised test ของคุณไม่บताได้ว่า behavior ยังถูกต้อง
เป็นไปได้ที่ assert ถ้า exception raised และถ้าไม่ raised ว่า exception ไหน
# expecting_exceptions_spec.rb
# RSpec example
def foo
raise NotImplementedError, "foo method not implemented"
end
describe "#foo" do
it "raises a RuntimeError" do
expect { foo }.to raise_error(RuntimeError)
end
end
1) #foo raises a RuntimeError
Failure/Error: expect { foo }.to raise_error(RuntimeError)
expected RuntimeError, got #
with backtrace:
# ./expecting_exceptions_spec.rb:4:in `foo'
# ./expecting_exceptions_spec.rb:9:in `block (3 levels) in '
# ./expecting_exceptions_spec.rb:9:in `block (2 levels) in '
# ./expecting_exceptions_spec.rb:9:in `block (2 levels) in '
Re-raise Exception
Application ควร capture exceptions สูงถึง Exception class แค่เมื่อมี very good reason ตัวอย่างเมื่อมี cleanup involved ก่อน exit block of code เช่น remove temporary files ที่ really ต้อง remove
One recommendation เมื่อคุณ absolutely ต้อง rescue Exception re-raise หลังจาก done handling error วิธีนี้ Ruby exception handling decide fate ของ process หลัง
File.open("/tmp/my_app.status", "w") { |f| "running" }
begin
foo
rescue Exception => e
Appsignal.add_error e
File.open("/tmp/my_app.status", "w") { |f| "stopped" }
raise e
end
Unsure What to Rescue?
เหมือนกล่าวไว้ก่อนหน้า มันดีที่จะ specific ใน errors ที่ rescue
เมื่อคุณ unsure ว่าการ operate ใด raise exceptions อะไร rescue StandardError เป็น good place to start รันโค้ดใน scenarios ต่างๆ ดูว่ามัน raise exceptions อะไร
begin
File.open('/tmp/appsignal.log', 'a') { |f| f.write "Starting AppSignal" }
rescue => e
puts e.inspect
end
#
ทุกครั้งที่เจอ exception ใหม่ เพิ่ม specific rescue cases สำหรับ exceptions นั้นๆ หรือ relevant parent class มันดีกว่า rescue too many exceptions
begin
file = '/tmp/appsignal.log'
File.open(file, 'a') { |f| f.write("AppSignal started!") }
rescue Errno::ENOENT => e
puts "File or directory #{file} doesn't exist."
rescue Errno::EACCES => e
puts "Cannot write to #{file}. No permissions."
end
# Or, using the parent error class
begin
file = '/tmp/appsignal.log'
File.open(file, 'a')
rescue SystemCallError => e
puts "Error while writing to file #{file}."
puts e
end
นี่คือสรุป primer on exceptions handling ใน Ruby บอกเราที่ @AppSignal หากอยากรู้เพิ่ม หรือมีคำถามเฉพาะ หากอยากได้ better insight ว่า exceptions raised ที่ไหนและบ่อยแค่ไหนใน app ลอง AppSignal ดูได้