はじめに
実務でバグ改修をしているときにcontroller内に次のような処理を見つけました。
def create
begin
# (通常のcreateの処理)
rescue エラーの種類 => e
# (eを使って色々書いてある)
end
end
beginというものが突如現れました。これはなんぞや?ifたいなものなのか・・・?
これまでRubyの中でbeginなんて記述をしたことがなかったので衝撃を受けました。
このままではバグ改修もできないので、「僕が出会ったこのbeginというものを僕はどれくらい知ってるんだろ〜」と思い、調べてみることにしました。
そうするとbeginを使用する際は基本的にrescueもセットであることもわかりましたので備忘録として残したいと思います。
もしもおかしな点や補足等ありましたら教えていただけると幸いです。
環境(自分のPC)
Ruby 2.6.5
Rails 6.0.5
前提
まず、singerモデル及びsingersコントローラーを作成し、singer.html.erbファイル内のボックスに値を入力すると値を登録できる簡単な機能を実装します。
class SingersController < ApplicationController
def index
@singers = Singer.all
end
def new
@singer = Singer.new
end
def create
@singer = Singer.new(singer_params)
@singer.save
redirect_to root_path
end
private
def singer_params
params.require(:singer).permit(:name, :famous_song)
end
end
画面の動きは次のようになりました。
試しに値を登録してみます。
データベース上にもきちんと登録されてそうです。
beginとは
公式リファレンスでbeginは「例外処理」に分類されています。
その中では次ように記されています。
本体の実行中に例外が発生した場合、rescue 節(複数指定できます)が与えられていれば例外を捕捉できます。発生した例外と一致する rescue 節が存在する時には rescue 節の本体が実行されます。
ほ、ほう。やるな。先ほどのコードで見てみましょう。
def create
begin
# (通常のcreateの処理)
rescue エラーの種類 => e
# (eを使って色々書いてある)
end
end
- beginからrescueまで
- この中には通常の処理を記載します。今回はcreateアクションですので、値を登録するコード等を書きます。
- rescueからendまで
- この中にはbeginからrescueまでの処理がエラーになってしまったり、なんらかの理由で登録できなかったといった場合に例外的に発生させる処理を記述します。
ちなみに下記部分のeはエラーデータが格納されているようです。
rescue エラーの種類 => e
# (eを使って色々書いてある)
end
文字だけではわからないので今回もやってみることにしましょう!
いろいろやってみる
createアクションの中に次のようなコードを追記します。
class SingersController < ApplicationController
# (省略)
def create
@singer = Singer.new(singer_params)
# ここから追記-------------------------
raise SyntaxError, "invalid syntax"
# ここまで追記-------------------------
@singer.save
redirect_to root_path
end
# (省略)
end
上で記述したraiseは強制的に例外を発生させる記述です。
- raiseとは
raise #第一の形式
raise messageまたはexception #第二の形式
raise error_type, message #第三の形式
raise error_type, message, traceback #第四の形式
例外を発生させます。第一の形式では直前の例外を再発生させます。第二の形式では、引数が文字列であった場合、その文字列をメッセージとする RuntimeError 例外を発生させます。引数が例外オブジェクトであった場合にはその例外を発生させます。第三の形式では第一引数で指定された例外を、第二引数をメッセージとして発生させます。第四の形式の第三引数は $@または Kernel.#callerで得られるスタック情報で、例外が発生した場所を示します。
上の場合だと、"invalid syntax"をメッセージとする SyntaxError例外を発生させる記述となります。
この状態で値を登録しようとするとSyntaxErrorが発生します。
しかし、発生した例外は後述のbegin式のrescue節で捕らえることができます。
その場合 rescue error_type => var の形式を使えば例外オブジェクトを得られます。
次のように記述してみます。
class SingersController < ApplicationController
# (省略)
def create
begin
@singer = Singer.new(singer_params)
raise SyntaxError, "invalid syntax"
@singer.save
redirect_to root_path
rescue SyntaxError => e
Rails.logger.error("エラーの種類:#{e.class} エラーメッセージ:#{e.message}")
render "new"
end
end
# (省略)
end
beginからrescueまでの処理はSyntaxErrorが発生しますので、rescueからendまででSyntaxErrorが発生した場合の処理をします。
今回はRails.loggerでログを埋め込む処理とnew.html.erbに遷移する処理を記述しています。
e.classはエラークラスを、e.messageはエラーメッセージをそれぞれ出力しています。
少しわかりにくいですが、先程のように送信ボタンを押してもSyntaxErrorになることはなく、new.html.erbファイルに遷移(元々new.html.erbなので見た目は変わらない)されています。ログも確認してみます。
送信ボタンが押された後にログにRails.loggerによるエラーメッセージが出力され、さらにnew.html.erbに遷移していることがわかります。
beginとrescueって、そういうことだったのかーと納得しました。
さらに加えて
begin rescue以外にもensureとretryというものも学んだのでそちらも紹介しておきます。
ensure
例外の発生にかかわらず処理を実行したい場合はensureを記述します。ensureは一番最後記述する必要があります。
つまりは例外なんて関係ない!というときに使います。
def create
begin
@singer = Singer.new(singer_params)
# raise SyntaxError, "invalid syntax"
@singer.save
redirect_to root_path
rescue SyntaxError => e
Rails.logger.error("エラーの種類:#{e.class} エラーメッセージ:#{e.message}")
render "new"
ensure
Rails.logger.info "これだけは言わせて"
end
end
上記の場合であれば、コメントアウトしているraise文があろうがなかろうが、ensureからendまでのログは出力されます。実際にやってみます。
retry
retry は、rescue 節で begin 式をはじめからもう一度実行するのに使用します。 retry を使うことである処理が成功するまで処理を繰り返すようなループを作ることができます。
def create
begin
@singer = Singer.new(singer_params)
# raise SyntaxError, "invalid syntax"
@singer.save
redirect_to root_path
rescue SyntaxError => e
Rails.logger.error("エラーの種類:#{e.class} エラーメッセージ:#{e.message}")
render "new"
retry
# 例外になった場合は、ここで再代入等して初めに戻ってやり直す
end
end
retryからendまでの間で値の再代入等を行って、再びbeginから実行できます。記述の仕方によっては無限ループになってしまうのでご注意ください。
補足
エラーの種類による記載
rescue SyntaxError => e
こちらの書き方ですが、今回はeにエラーの値を格納しています。ここの記述はe出なくても問題あリません。
また、もちろんエラーはSyntaxErrorだけではありません。
- SyntaxError 文法エラー
- NoMethodError メソッドが存在しない
- ArgumentError 引数が適切でない(数や型が違う)
- ZeroDivisionError 整数を0で割った場合に発生
- NameError 未定義の変数や定数などを呼び出したときに発生
- LoadError requireやload失敗時のエラー
- RangeError 指定した値が範囲外の時に発生
- RegexpError 正規表現が無効な場合に発生
- RuntimeError 特定の例外クラスには該当しないエラーが起こったときに発生
- StandardError 通常のプログラムで発生する可能性の高いエラーを束ねるクラス
- Exceptoin 全ての例外の祖先になるクラス
などなど様々なエラーを記述できます。
複数エラーの例外処理
複数種類のエラーの例外処理をrescueしたい場合は下記のようにいくつも並べることができます。
begin
# ・・・
rescue LoadError => e
# ・・・
rescue ZeroDivisionError => e
# ・・・
end
例外処理が同じなら
begin
# ・・・
rescue LoadError, ZeroDivisionError => e
# ・・・
end
こうも書けるみたいです。
beginとendの省略
beginは省略することができます。
それに付随してendも忘れずに省略しましょう。
class SingersController < ApplicationController
# (省略)
def create
begin # 省略可
@singer = Singer.new(singer_params)
raise SyntaxError, "invalid syntax"
@singer.save
redirect_to root_path
rescue SyntaxError => e
Rails.logger.error("エラーの種類:#{e.class} エラーメッセージ:#{e.message}")
render "new"
end # beginを省略するのであれば要省略
end
# (省略)
end
class SingersController < ApplicationController
# (省略)
def create
@singer = Singer.new(singer_params)
raise SyntaxError, "invalid syntax"
@singer.save
redirect_to root_path
rescue SyntaxError => e
Rails.logger.error("エラーの種類:#{e.class} エラーメッセージ:#{e.message}")
render "new"
end
# (省略)
end
メソッド全体での例外処理となります。
注意点
コメントにて@scivolaさんに指摘をいただいているようにbeginからrescueの間はできるだけボリュームを持たせないほうがいいそうです。
理由はどこで発生した例外なのかの判断が難しくなり、エラーの箇所の特定が遅れてしまうからだそうです。
気をつけていきたいところです!
おわりに
今回はbegin rescueについて学びました。
rescueについてはいつももやもやしていたので調べることができてよかったです。
エンジニアになって3ヶ月が経ちました。知らないことは増えていく一方です。でも、できることを増やしていけるようにこれからも学び続けたいと思います。
参考記事
参考歌手