Rails controller内に登場したbegin rescueとはなんぞや

#### はじめに
実務でバグ改修をしているときにcontroller内に次のような処理を見つけました。
~~~ ruby
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ファイル内のボックスに値を入力すると値を登録できる簡単な機能を実装します。
~~~ singers_controller.rb
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
~~~
画面の動きは次のようになりました。
試しに値を登録してみます。
[![Image from Gyazo](https://i.gyazo.com/e0f9991642f74eee82d2a462695ad2c3.gif)](https://gyazo.com/e0f9991642f74eee82d2a462695ad2c3)
データベース上にもきちんと登録されてそうです。
[![Image from Gyazo](https://i.gyazo.com/41e6317f15a6475c057459977bbb20d6.png)](https://gyazo.com/41e6317f15a6475c057459977bbb20d6)
#### beginとは
公式リファレンスでbeginは「例外処理」に分類されています。
その中では次ように記されています。
>本体の実行中に例外が発生した場合、rescue 節(複数指定できます)が与えられていれば例外を捕捉できます。発生した例外と一致する rescue 節が存在する時には rescue 節の本体が実行されます。

ほ、ほう。やるな。先ほどのコードで見てみましょう。
~~~ ruby
def create
  begin
    # （通常のcreateの処理）
  rescue エラーの種類 => e
    # （eを使って色々書いてある）
  end
end
~~~

- beginからrescueまで
    - この中には通常の処理を記載します。今回はcreateアクションですので、値を登録するコード等を書きます。
- rescueからendまで
    - この中にはbeginからrescueまでの処理がエラーになってしまったり、なんらかの理由で登録できなかったといった場合に例外的に発生させる処理を記述します。

ちなみに下記部分のeはエラーデータが格納されているようです。
~~~ ruby
  rescue エラーの種類　=> e
    # （eを使って色々書いてある）
  end
~~~

文字だけではわからないので今回もやってみることにしましょう！

#### いろいろやってみる
createアクションの中に次のようなコードを追記します。
~~~ singers_controller.rb
class SingersController < ApplicationController
# （省略）
  def create
    @singer = Singer.new(singer_params)
    # ここから追記-------------------------
    raise SyntaxError, "invalid syntax"
    # ここまで追記-------------------------
    @singer.save
    redirect_to root_path   
  end
# （省略）
end
~~~
上で記述したraiseは強制的に例外を発生させる記述です。
- raiseとは
~~~ ruby
raise #第一の形式
raise messageまたはexception #第二の形式
raise error_type, message #第三の形式
raise error_type, message, traceback #第四の形式
~~~
>例外を発生させます。第一の形式では直前の例外を再発生させます。第二の形式では、引数が文字列であった場合、その文字列をメッセージとする RuntimeError 例外を発生させます。引数が例外オブジェクトであった場合にはその例外を発生させます。第三の形式では第一引数で指定された例外を、第二引数をメッセージとして発生させます。第四の形式の第三引数は $@または Kernel.#callerで得られるスタック情報で、例外が発生した場所を示します。

上の場合だと、"invalid syntax"をメッセージとする SyntaxError例外を発生させる記述となります。
この状態で値を登録しようとするとSyntaxErrorが発生します。
[![Image from Gyazo](https://i.gyazo.com/fe7cfb6730e1687a8062e5fa7993e6db.gif)](https://gyazo.com/fe7cfb6730e1687a8062e5fa7993e6db)
しかし、発生した例外は後述のbegin式のrescue節で捕らえることができます。
その場合 rescue error_type => var の形式を使えば例外オブジェクトを得られます。
次のように記述してみます。
~~~ singers_controller.rb
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はエラーメッセージをそれぞれ出力しています。
[![Image from Gyazo](https://i.gyazo.com/592a0faa43426704c3517a1326364eae.gif)](https://gyazo.com/592a0faa43426704c3517a1326364eae)
少しわかりにくいですが、先程のように送信ボタンを押してもSyntaxErrorになることはなく、new.html.erbファイルに遷移（元々new.html.erbなので見た目は変わらない）されています。ログも確認してみます。
[![Image from Gyazo](https://i.gyazo.com/2a058c5d602ac46dc7cb328716a3f668.png)](https://gyazo.com/2a058c5d602ac46dc7cb328716a3f668)
送信ボタンが押された後にログにRails.loggerによるエラーメッセージが出力され、さらにnew.html.erbに遷移していることがわかります。
beginとrescueって、そういうことだったのかーと納得しました。
#### さらに加えて
begin rescue以外にもensureとretryというものも学んだのでそちらも紹介しておきます。
##### ensure
>例外の発生にかかわらず処理を実行したい場合はensureを記述します。ensureは一番最後記述する必要があります。

つまりは例外なんて関係ない！というときに使います。
~~~ singers_controller.rb
 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までのログは出力されます。実際にやってみます。
- エラーを発生させず正常に登録された場合のログ
[![Image from Gyazo](https://i.gyazo.com/cfcb978193696a9e5f839870ce240640.png)](https://gyazo.com/cfcb978193696a9e5f839870ce240640)
- raiseで例外を発生させた場合のログ
[![Image from Gyazo](https://i.gyazo.com/58d8f0d76b5e57893692c600364e38b0.png)](https://gyazo.com/58d8f0d76b5e57893692c600364e38b0)
いずれにせよ"これだけは言わせて"というログが出力されています。

##### retry
>retry は、rescue 節で begin 式をはじめからもう一度実行するのに使用します。 retry を使うことである処理が成功するまで処理を繰り返すようなループを作ることができます。
~~~ singers_controller.rb
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から実行できます。記述の仕方によっては無限ループになってしまうのでご注意ください。
[![Image from Gyazo](https://i.gyazo.com/8fa39274901b42d749dad4b63046ed3f.png)](https://gyazo.com/8fa39274901b42d749dad4b63046ed3f)
#### 補足
##### エラーの種類による記載
~~~ ruby
rescue SyntaxError => e
~~~
こちらの書き方ですが、今回はeにエラーの値を格納しています。ここの記述はe出なくても問題あリません。
また、もちろんエラーはSyntaxErrorだけではありません。
- SyntaxError	文法エラー
- NoMethodError	メソッドが存在しない
- ArgumentError	引数が適切でない（数や型が違う）
- ZeroDivisionError	整数を0で割った場合に発生
- NameError	未定義の変数や定数などを呼び出したときに発生
- LoadError	requireやload失敗時のエラー
- RangeError	指定した値が範囲外の時に発生
- RegexpError	正規表現が無効な場合に発生
- RuntimeError	特定の例外クラスには該当しないエラーが起こったときに発生
- StandardError	通常のプログラムで発生する可能性の高いエラーを束ねるクラス
- Exceptoin	全ての例外の祖先になるクラス

などなど様々なエラーを記述できます。

##### 複数エラーの例外処理

複数種類のエラーの例外処理をrescueしたい場合は下記のようにいくつも並べることができます。
~~~ ruby
begin
 # ・・・
rescue LoadError => e
 # ・・・
rescue ZeroDivisionError => e
 # ・・・
end
~~~
例外処理が同じなら
~~~ ruby
begin
 # ・・・
rescue LoadError, ZeroDivisionError => e
 # ・・・
end
~~~
こうも書けるみたいです。

##### beginとendの省略
beginは省略することができます。
それに付随してendも忘れずに省略しましょう。
~~~ singers_controller.rb
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
~~~
~~~ singers_controller.rb
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についてはいつももやもやしていたので調べることができてよかったです。
エンジニアになって３ヶ月が経ちました。知らないことは増えていく一方です。でも、できることを増やしていけるようにこれからも学び続けたいと思います。

#### 参考記事
https://docs.ruby-lang.org/ja/latest/doc/spec=2fcontrol.html#begin

https://prograshi.com/framework/rails/rescue_rais_retry_ensure/

https://qiita.com/ktarow/items/9d8f3217bb148f2e51d2

#### 参考歌手

https://www.mrchildren.jp/

https://www.begin1990.com/