Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

เชี่ยวชาญ Exception Handling ใน Ruby: คู่มือปฏิบัติจริง

ที่ 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 ... end block มันไม่สามารถ 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 ดูได้