Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Rubyでの例外の救済:入門書

AppSignalでは、Rubyアプリケーションのエラー追跡を提供しています。そのために、アプリケーションがスローするすべての例外をキャプチャし、発生したときに開発者に通知します。

例外処理を正しく行うのは難しい場合があります。この記事では、それがどのように機能するか、不適切な処理が引き起こす可能性のある問題、および例外を適切に救済する方法について説明します。

例外の救済

Rubyで例外を救済することで、問題が発生した瞬間にアプリケーションがクラッシュするのを防ぐことができます。 begin .. rescue ブロックでは、エラーが発生したときにアプリケーションの代替パスを指定できます。

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

どの例外をレスキューするかを指定することもできます。例外クラスを指定すると、この例外のすべてのサブクラスもキャプチャされます。

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

上記の例では、例外Errno::ENOENTを確認できます。 親のSystemCallErrorがキャッチされる 救助されています。

例外チェーンの上位にある救助

例外チェーンの上位にある例外をレスキューしないことが重要です。これを行うと、サブクラス化されたすべての例外もキャッチされ、レスキューブロックのキャプチャが一般的になりすぎます。

これは、プログラムに渡された引数に基づいて構成ファイルを読み取るプログラムです。

# $ 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

エラーメッセージには、構成ファイルを読み取れなかったことが示されていますが、実際の問題はコードのタイプミスでした。

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

begin .. rescueによってキャッチされたデフォルトの例外クラス ブロックはStandardErrorです。特定のクラスを渡さない場合、RubyはStandardErrorとすべてのサブクラス化されたエラーをレスキューします。 NoMethodErrorはこれらのエラーの1つです。

特定の例外クラスをレスキューすると、無関係なエラーが誤って障害状態を引き起こすのを防ぐのに役立ちます。また、エンドユーザーにとってより役立つ、より具体的なカスタムエラーメッセージも可能になります。

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

例外の救済

それでも、例外チェーンの上位で救助したくなるかもしれません。アプリケーションが発生する可能性のあるすべてのエラーを救済することで、アプリケーションがクラッシュするのを防ぐことができます。 (ここに100%の稼働時間があります!)ただし、多くの問題が発生する可能性があります。

Exceptionクラスは、Rubyの主要な例外クラスです。他のすべての例外は、このクラスのサブクラスです。例外が救済された場合、すべてのエラーがキャッチされます。

ほとんどのアプリケーションが救済したくない2つの例外は、SignalExceptionとSystemExitです。

SignalExceptionは、外部ソースがアプリケーションに停止を指示している場合に使用されます。これは、シャットダウンする場合のオペレーティングシステム、またはアプリケーションを停止する場合のシステム管理者の場合があります。例

SystemExitは、exitの場合に使用されます Rubyアプリケーションから呼び出されています。これが発生すると、開発者はアプリケーションを停止する必要があります。例

例外をレスキューし、アプリケーションが現在実行中にこれらの例外が発生した場合、begin ... rescue ... end ブロックして終了できません。

通常の状況で例外を救済することは一般的に悪い考えです。 Exceptionをレスキューするときは、SignalExceptionとSystemExitが機能するのを防ぎますが、いくつか例を挙げると、LoadError、SyntaxError、NoMemoryErrorも機能しません。代わりに、より具体的な例外を救済することをお勧めします。

テストの失敗

例外がレスキューされたら、rescue Exception => eを使用します 、アプリケーション以外のものが壊れることがあります。テストスイートは実際にいくつかのエラーを隠している可能性があります。

minitestおよびRSpecアサーションで失敗すると、例外が発生して、失敗したアサーションについて通知し、テストに失敗します。その場合、Exceptionからサブクラス化された独自のカスタム例外を発生させます。

テストまたはアプリケーションコードで例外がレスキューされた場合、アサーションの失敗を黙らせている可能性があります。

# 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

例外が発生する可能性があります

一部のコードは、例外を発生させることを目的としています。テストスイートでは、例外が発生したときにテストが失敗しないようにするために、例外を単純に消音することができます。

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

ただし、これは例外が発生したかどうかをテストしません。例外が発生しない場合、テストでは動作がまだ正しいかどうかを判断できません。

例外が発生したかどうか、発生しなかった場合はどの例外が発生したかを表明することができます。

# 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)>'

例外の再発生

アプリケーションは、非常に正当な理由がある場合にのみ、チェーンの上位にあるExceptionクラスの例外のみをキャプチャする必要があります。たとえば、実際に削除する必要のある一時ファイルを削除するなど、コードのブロックを終了する前にクリーンアップが必要な場合です。

どうしても例外を救出する必要がある場合の1つの推奨事項は、エラーの処理が完了した後で例外を再発生させることです。このようにして、Rubyの例外処理でプロセスの運命を後で決定できます。

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

何を救うべきかわからない?

先に述べたように、どのエラーを救済するかを具体的に示すのは良いことです。

操作で発生する可能性のある例外がわからない場合は、StandardErrorのレスキューを開始することをお勧めします。さまざまなシナリオでコードを実行し、どのような例外が発生するかを確認します。

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>

新しい例外に遭遇するたびに、それらの例外またはそれに関連する親クラスの特定のレスキューケースを追加します。あまりにも多くの例外を救済するよりも、何を救済するかを具体的にする方がよいでしょう。

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

これで、Rubyでの例外処理に関する入門書は終わりです。詳細を知りたい場合、または具体的な質問がある場合は、@AppSignalまでお知らせください。アプリで例外が発生する場所と頻度についてより良い洞察を得たい場合は、AppSignalを試してみてください。


  1. Rubyのカスタム例外

    Rubyで独自の例外を作成するのは簡単です。次の手順に従ってください: 1。新しいクラスを作成する 例外は、Rubyの他のすべてと同じように、クラスです。新しい種類の例外を作成するには、StandardErrorまたはその子の1つから継承するクラスを作成するだけです。 class MyError < StandardError end raise MyError 慣例により、新しい例外のクラス名は「エラー」で終わります。カスタム例外をモジュール内に配置することもお勧めします。つまり、最終的なエラークラスは次のようになります:ActiveRecord::RecordNotFound

  2. Rubyで例外にコンテキストデータを追加する方法

    標準のバックトレース/エラーメッセージの組み合わせでは不十分な場合があります。エラーの原因を特定するために、追加のデータが必要になる場合があります。幸い、Rubyで行うのはとても簡単です。 エラーメッセージのカスタマイズ エラーにコンテキスト情報を追加する最も簡単な方法は、それを例外のメッセージに追加することです。以下の例では、例外をキャッチし、新しいメッセージで再発生させています: begin raise foo rescue => e raise e.class, bar end # RuntimeError: bar このアプローチの良い使用例は、テンプレートをレン