LoginSignup
4
2

More than 3 years have passed since last update.

Rubyの例外のうち StandardError に属さないものを発生させる実験

Last updated at Posted at 2019-10-12

Rubyの例外クラスは全て Exception のサブクラスだが、そのうちプログラムの継続実行が困難なエラーStandardError のサブクラスではなく、例外の種類を省略した rescue では捕捉しないようになっている。

制御構造 例外処理: begin - Rubyリファレンスマニュアル

例外の一致判定は,発生した例外が rescue 節で指定したクラスのインスタンスであるかどうかで行われます。

error_type が省略された時は StandardError のサブクラスである全ての例外を捕捉します。Rubyの組み込み例外は(SystemExitInterrupt のような脱出を目的としたものを除いて) StandardError のサブクラスです。

test_rescue.rb
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 時に例外の種類を明示すれば簡単に発生させられるが、それ以外の場合では(一部を除いて)滅多に見ることがない。実際の例を見てみたくなったので試してみる。

例外の一覧

実際にコードを実行して一覧を手に入れる。

list_exceptions.rb
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

GemMonitorMixin にも例外が存在するようだが、その他はリファレンスマニュアルの組み込みライブラリの内容と一致する。

例外発生実験

raise で明示せず出せる例外を試していく。コードは全て beginrescueend にしてあり、 rescue Exception => e と直せば例外を捕捉して表示する。

Exception

全ての例外の祖先のクラスです。

恐らく継承のために用意しているもので、明示せずに出せるものではないと思う。

NoMemoryError

メモリの確保に失敗すると発生します。

という単純明快な例外。プログラムを継続実行しづらいことも納得しやすい。

しかしメモリを大量に消費しないといけないので、他のプロセスにも影響が出そうで気軽に試すのは怖い。以下はWindows Subsystem for Linux (WSL)上で影響なく動いたが、実行は自己責任で。

no_memory_error.rb
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.#requireKernel.#load が失敗したときに発生します。

ライブラリ名を間違えたりすれば出るので、よくお世話になる。

load_error.rb
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のソースコードで NotImplementedErrorrb_eNotImpError を検索すると、いくつかの場所で使われていることがわかる。

not_implemented_error.rb
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 内でわざと間違えることにする。

syntax_error.rb
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)
terminal
$ 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 で試してみる。さっきと同じ文法エラーのコードを与えても、「汚染させた」場合はそもそも実行されなくなる。

security_error.rb
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 を送ってみる。

signal_exeption.rb
begin
    Process.kill(:TERM, Process.pid)
rescue => e
    p e
end
# Terminated

リファレンスマニュアルに書いてあるが、全てのシグナルに対応するわけではない。例えば SIGSEGV を送ると例外すら出ずRubyがコアダンプして異常終了する。

Interrupt

SIGINT シグナルを捕捉していないときに SIGINT シグナルを受け取ると発生します。 SIGINT 以外のシグナルを受信したときに発生する例外については SignalException を参照してください。

こちらはコード実行を Ctrl+C で中断したりすれば出るのでよく見かける。

interrupt.rb
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.#exitKernel.#abort 、メインスレッドに対する Thread.kill などは SystemExit 例外を発生させます

ということは、スクリプトの途中で exit などしても捕捉すれば終了を取り消せる。

逆に、例外として異常終了させる(Tracebackを表示させる)ことはできなさそう。

system_exit.rb
begin
    abort 'test'
rescue => e
    p e
end
# test
terminal
$ ruby system_exit.rb # Exceptionを捕捉しない場合
test
$ echo $?             # --> abortで終わったので終了ステータスは0でない
1

$ ruby system_exit.rb # Exceptionを捕捉した場合
test
#<SystemExit: test>
$ echo $?             # --> スクリプト終端に達したので終了ステータスは0
0

SystemStackError

システムスタックがあふれたときに発生します。

典型的には、メソッド呼び出しを無限再帰させてしまった場合に発生します。

再帰の練習などしていれば見るはず。

system_stack_error.rb
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

fatal.rb
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>'
4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2