Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> Ruby

Thành Thạo Exception Handling Trong Ruby: Hướng Dẫn Thực Tế

Tại AppSignal chúng tôi provide error tracking cho Ruby applications. Để làm vậy, capture all exceptions applications throw và notify developers khi xảy ra.

Có thể difficult get exception handling right. Bài này giải thích cách work, problems bad handling cause, và rescue exceptions properly.

Rescuing Exceptions

Bằng cách rescue exceptions Ruby, bạn prevent application crash khi something goes wrong. Với block begin .. rescue specify alternative path ứng dụng khi error occurs.

begin File.read "config.yml" rescue puts "No config file found. Using defaults." end

Cũng possible specify exceptions nào should be rescued. Khi specify exception class, all subclasses cũng captured.

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

Ví dụ trên thấy exception Errno::ENOENT caught khi parent SystemCallError rescued.

Rescuing Too High Up Exception Chain

Quan trọng không rescue exceptions too high up Exception chain. Khi làm đó, all subclassed exceptions cũng caught, làm rescue block capture too generic.

Chương trình đọc config file based on argument passed.

# $ 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 nói couldn't read config file, nhưng real problem là typo trong code.

begin File.read config_file rescue => e puts e.inspect end #<NoMethodError: undefined method `firs' for []:Array>

Default exception class caught by begin .. rescue block là StandardError. Nếu không pass specific class, Ruby rescue StandardError và all subclassed errors. NoMethodError là một trong những errors này.

Rescue specific exception class giúp prevent unrelated errors accidentally prompt failure state. Cũng allow specific custom error messages helpful cho 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

Vẫn tempting rescue high up exception chain. Rescue all errors app raise prevent crash. (100% uptime here we come!) Tuy nhiên, cause nhiều problems.

Exception class là main exception class Ruby. All other exceptions subclass của class này; nếu Exception rescued all errors caught.

Hai exceptions most apps không muốn rescue: SignalException và SystemExit.

SignalException dùng khi outside source tell application stop. Có thể OS khi muốn shutdown, hoặc system administrator muốn stop application.

SystemExit dùng khi exit called từ Ruby application. Khi raise developer muốn application stop.

Nếu rescue Exception và exceptions này raised while app đang chạy begin ... rescue ... end block nó không thể exit.

Generally bad idea rescue Exception normal situations. Khi rescue Exception, prevent SignalException và SystemExit function, cũng như LoadError, SyntaxError, NoMemoryError, v.v. Better rescue specific exceptions.

Failures Trong Tests

Khi Exception rescued, dùng rescue Exception => e, things beside app could break. Test suite actually hide some errors.

Trong minitest và RSpec assertions fail raise exception inform failed assertion, failing test. Khi đó, raise own custom exceptions, subclassed from Exception.

Nếu Exception rescued trong test hoặc application code, could silence assertion failure.

# RSpec example def foo(bar) bar.baz rescue Exception => e puts "This test should actually fail" # Failure/Error: bar.baz # <Double (anonymous)> 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

Some code meant raise exceptions. Test suite possible simply silence exception để test not fail khi raised.

def foo raise RuntimeError, "something went wrong" end foo rescue RuntimeError

Tuy nhiên, không test nếu exception raised hay không. Khi exception không raised, test không tell behavior còn correct.

Possible assert nếu exception raised, và nếu không, exception nào.

# 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 #<NotImplementedError: foo method not implemented> with backtrace: # ./expecting_exceptions_spec.rb:4:in `foo' # ./expecting_exceptions_spec.rb:9:in `block (3 levels) in <top (required)>' # ./expecting_exceptions_spec.rb:9:in `block (2 levels) in <top (required)>' # ./expecting_exceptions_spec.rb:9:in `block (2 levels) in <top (required)>' 

Re-raise Exception

Application chỉ capture exceptions high up chain Exception class khi very good reason. Ví dụ, cleanup involved trước exit block code, như remove temporary files really cần remove.

One recommendation khi absolutely phải rescue Exception, re-raise sau done handling error. Cách này Ruby exception handling decide fate of process afterward.

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?

Như mentioned earlier, good be specific errors rescue.

Khi unsure operation raise exceptions gì, rescue StandardError good place start. Run code different scenarios xem raise exceptions gì.

begin File.open('/tmp/appsignal.log', 'a') { |f| f.write "Starting AppSignal" } rescue => e puts e.inspect end #<Errno::EACCES: Permission denied @ rb_sysopen - /tmp/appsignal.log>

Mỗi lần gặp exception mới, add specific rescue cases cho exceptions đó hoặc relevant parent class. Better specific what rescue than 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 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

Kết thúc primer exception handling Ruby. Liên hệ @AppSignal nếu muốn biết thêm, hoặc specific question. Muốn insight better where/how often exceptions raised in app, try AppSignal.