Rubyの例外クラスは全て Exception
のサブクラスだが、そのうちプログラムの継続実行が困難なエラーは StandardError
のサブクラスではなく、例外の種類を省略した rescue
では捕捉しないようになっている。
制御構造 例外処理: begin - Rubyリファレンスマニュアル
例外の一致判定は,発生した例外が rescue 節で指定したクラスのインスタンスであるかどうかで行われます。
error_type が省略された時は
StandardError
のサブクラスである全ての例外を捕捉します。Rubyの組み込み例外は(SystemExit
やInterrupt
のような脱出を目的としたものを除いて)StandardError
のサブクラスです。
begin
raise StandardError, 'test'
rescue => e
p e
end
#=> #<StandardError: test>
begin
raise Exception, 'test'
rescue => e
p e
end
# Traceback (most recent call last):
# test_rescue.rb:9:in `<main>': test (Exception)
もちろんこの例のように raise
時に例外の種類を明示すれば簡単に発生させられるが、それ以外の場合では(一部を除いて)滅多に見ることがない。実際の例を見てみたくなったので試してみる。
例外の一覧
実際にコードを実行して一覧を手に入れる。
pp ObjectSpace.each_object(Class)
.select { |cls| cls <= Exception }
.reject { |cls| cls <= StandardError }
.group_by(&:superclass)
結果を継承関係で木構造にまとめると次の通りになる。
-
Exception
NoMemoryError
-
ScriptError
-
LoadError
-
Gem::LoadError
Gem::ConflictError
-
Gem::MissingSpecError
Gem::MissingSpecVersionError
-
NotImplementedError
SyntaxError
-
SecurityError
-
SignalException
Interrupt
-
SystemExit
Gem::SystemExitException
SystemStackError
fatal
MonitorMixin::ConditionVariable::Timeout
Gem
や MonitorMixin
にも例外が存在するようだが、その他はリファレンスマニュアルの組み込みライブラリの内容と一致する。
例外発生実験
raise
で明示せず出せる例外を試していく。コードは全て begin
~ rescue
~ end
にしてあり、 rescue Exception => e
と直せば例外を捕捉して表示する。
Exception
全ての例外の祖先のクラスです。
恐らく継承のために用意しているもので、明示せずに出せるものではないと思う。
NoMemoryError
メモリの確保に失敗すると発生します。
という単純明快な例外。プログラムを継続実行しづらいことも納得しやすい。
しかしメモリを大量に消費しないといけないので、他のプロセスにも影響が出そうで気軽に試すのは怖い。以下はWindows Subsystem for Linux (WSL)上で影響なく動いたが、実行は自己責任で。
begin
String.new(capacity: 2 ** 40) # 1 TiB
rescue => e
p e
end
# Traceback (most recent call last):
# no_memory_error.rb: failed to allocate memory (NoMemoryError)
ScriptError
スクリプトのエラーを表す例外クラスです。
以下の例外クラスのスーパークラスです。
LoadError
NotImplementedError
SyntaxError
これらの例外が発生したときは Ruby スクリプト自体にバグがある可能性が高いと考えられます。
これも恐らく継承のために用意しているもので、明示せずに出せるものではないと思う。
LoadError
Kernel.#require
やKernel.#load
が失敗したときに発生します。
ライブラリ名を間違えたりすれば出るので、よくお世話になる。
begin
require 'this/file/does/not/exist'
rescue => e
p e
end
# Traceback (most recent call last):
# 2: from load_error.rb:2:in `<main>'
# 1: from $(gem environment gemdir)/rubygems/core_ext/kernel_require.rb:54:in `require'
# $(gem environment gemdir)/rubygems/core_ext/kernel_require.rb:54:in `require': cannot load such file -- this/file/does/not/exist (LoadError)
NotImplementedError
実装されていない機能が呼び出されたときに発生します。
恐らくユーザー(やライブラリ)が使うために用意された例外。明示する以外に出す方法は無いと思う。Rubyのソースコードで NotImplementedError
や rb_eNotImpError
を検索すると、いくつかの場所で使われていることがわかる。
begin
require 'digest'
puts Digest::Base.hexdigest('test')
rescue => e
p e
end
# Traceback (most recent call last):
# 2: from not_implemented_error.rb:3:in `<main>'
# 1: from not_implemented_error.rb:3:in `hexdigest'
# not_implemented_error.rb:3:in `digest': Digest::Base is an abstract class (NotImplementedError)
これまで未実装の部分やオーバーライド前提のメソッドに raise 'Not implemented.'
なんて仕込んできた(→到達すると RuntimeError
になる)が、こっちの例外を使うのが正しかった?
SyntaxError
ソースコードに文法エラーがあったときに発生します。
これもよくお世話になる。とはいえ試すとなると、コード読み込み時に文法チェックに引っかかったら実行できないので、 eval
内でわざと間違えることにする。
begin
eval ')'
rescue => e
p e
end
# Traceback (most recent call last):
# 1: from syntax_error.rb:2:in `<main>'
# syntax_error.rb:2:in `eval': (eval):1: syntax error, unexpected ')' (SyntaxError)
$ ruby -c syntax_error.rb
Syntax OK
$ ruby syntax_error.rb
Traceback (most recent call last):
1: from syntax_error.rb:2:in `<main>'
syntax_error.rb:2:in `eval': (eval):1: syntax error, unexpected ')' (SyntaxError)
SecurityError
セキュリティ上の問題が起きたときに発生します。
セキュリティモデルも参照してください。
使ったことのない仕組みなのでピンとこないが、とりあえず eval
で試してみる。さっきと同じ文法エラーのコードを与えても、「汚染させた」場合はそもそも実行されなくなる。
begin
$SAFE = 1
eval ')'.taint
rescue => e
p e
end
# Traceback (most recent call last):
# 1: from security_error.rb:3:in `<main>'
# security_error.rb:3:in `eval': Insecure operation - eval (SecurityError)
SignalException
捕捉していないシグナルを受け取ったときに発生します。
Signal.#trap
で割り込みシグナルをハンドリングしないと発生する。
Process.#kill
で自身のプロセスに SIGTERM を送ってみる。
begin
Process.kill(:TERM, Process.pid)
rescue => e
p e
end
# Terminated
リファレンスマニュアルに書いてあるが、全てのシグナルに対応するわけではない。例えば SIGSEGV を送ると例外すら出ずRubyがコアダンプして異常終了する。
Interrupt
SIGINT シグナルを捕捉していないときに SIGINT シグナルを受け取ると発生します。 SIGINT 以外のシグナルを受信したときに発生する例外については
SignalException
を参照してください。
こちらはコード実行を Ctrl+C で中断したりすれば出るのでよく見かける。
begin
Process.kill(:INT, Process.pid)
rescue => e
p e
end
# Traceback (most recent call last):
# 1: from interrupt.rb:2:in `<main>'
# interrupt.rb:2:in ``': Interrupt
SystemExit
Ruby インタプリタを終了させるときに発生します。
例外自身のページよりも終了処理のページのほうが説明が詳しい。
関数
Kernel.#exit
やKernel.#abort
、メインスレッドに対するThread.kill
などはSystemExit
例外を発生させます
ということは、スクリプトの途中で exit
などしても捕捉すれば終了を取り消せる。
逆に、例外として異常終了させる(Tracebackを表示させる)ことはできなさそう。
begin
abort 'test'
rescue => e
p e
end
# test
$ ruby system_exit.rb # Exceptionを捕捉しない場合
test
$ echo $? # --> abortで終わったので終了ステータスは0でない
1
$ ruby system_exit.rb # Exceptionを捕捉した場合
test
#<SystemExit: test>
$ echo $? # --> スクリプト終端に達したので終了ステータスは0
0
SystemStackError
システムスタックがあふれたときに発生します。
典型的には、メソッド呼び出しを無限再帰させてしまった場合に発生します。
再帰の練習などしていれば見るはず。
def factorial(n)
return 1 if n == 0
n * factorial(n - 1)
end
begin
p factorial(-1)
rescue => e
p e
end
# Traceback (most recent call last):
# 10080: from system_stack_error.rb:8:in `<main>'
# 10079: from system_stack_error.rb:4:in `factorial'
# 10078: from system_stack_error.rb:4:in `factorial'
# 10077: from system_stack_error.rb:4:in `factorial'
# 10076: from system_stack_error.rb:4:in `factorial'
# 10075: from system_stack_error.rb:4:in `factorial'
# 10074: from system_stack_error.rb:4:in `factorial'
# 10073: from system_stack_error.rb:4:in `factorial'
# ... 10068 levels...
# 4: from system_stack_error.rb:4:in `factorial'
# 3: from system_stack_error.rb:4:in `factorial'
# 2: from system_stack_error.rb:4:in `factorial'
# 1: from system_stack_error.rb:4:in `factorial'
# system_stack_error.rb:4:in `factorial': stack level too deep (SystemStackError)
fatal
インタプリタ内部で致命的なエラーが起こったときに発生します。
致命的なエラーとは、例えば以下のような状態です。
- スレッドのデッドロックが発生した
- -x オプションや -C オプションで指定されたディレクトリに移動できなかった
- -i オプション付きで起動されたが、 パーミッションなどの関係でファイルを変更できなかった
通常の手段では、 Ruby プログラムからは fatal クラスにはアクセスできません。
小文字で始まっているけれどもクラス。モジュール全体を探しても小文字始まりはこれしか無い。普通にコードに fatal
と書いても、変数かメソッドと判断されて例外を参照できない。
ary = ObjectSpace.each_object(Module).select { |mdl| /^[a-z]/ =~ mdl.to_s }
e = ary.first
raise e, 'test'
致命的なエラーとして挙げられているデッドロックを起こしてみる。(参考:The Backyard - DeadLockInRuby)
begin
q = Queue.new
Thread.new { q.deq }.join
rescue => e
p e
end
# Traceback (most recent call last):
# 1: from fatal.rb:3:in `<main>'
# fatal.rb:3:in `join': No live threads left. Deadlock? (fatal)
# 2 threads, 2 sleeps current:0x00007fffcf775500 main thread:0x00007fffcf3b7470
# * #<Thread:0x00007fffcf3e72c8 sleep_forever>
# rb_thread_t:0x00007fffcf3b7470 native:0x00007f9e3def0700 int:0
# fatal.rb:3:in `join'
# fatal.rb:3:in `<main>'
# * #<Thread:0x00007fffcf7ac188@fatal.rb:3 sleep_forever>
# rb_thread_t:0x00007fffcf775500 native:0x00007f9e39f90700 int:0
# depended by: tb_thread_id:0x00007fffcf3b7470
# fatal.rb:3:in `pop'
# fatal.rb:3:in `block in <main>'