錯誤處理( error handling )


#1

和其他所有程式一樣,Ruby 也有自己的錯誤處理機制,不過相對起來非常方便及好用,這篇基本上只有幾個 keyword

begin / rescue / $! , $@ / retry / raise / 還有一堆 Exception 相關的 Class

首先,最基本的方式

begin
  error
rescue
  puts 'QwQ error'
end
#=> QwQ error

簡單的來說 begin 和 rescue 內包裝會錯誤的部分,而 rescue 到 end 部分包裝如果錯誤時要做啥

再來是 retry 的用法

begin
  error
rescue
  @error_count ||= 0
    #這行全等於 @error_count = 0 unless @error_count來用於初始化
  @error_count += 1
  if @error_count < 10
    puts @error_count
    retry
  end
  puts 'QwQ Error too...'
end
#=> 1 \n 2 \n 3 ... \n 9 \n QwQ Error too...

很簡單方便的用法,不過記得 retry 一定要設定終止條件,否則其作用就和 while true 與 loop 同義

再來會發現,如果我用了 rescue 其實不會有 error log & trace 出現,可以用這種方式來進行重現

begin
  error
rescue
  puts $!.class
  puts $!
  puts $@.class
  puts $@
  # puts $! , %@  ##一般時候這樣用就好,一次顯示
end
#=> NameError
#=> undefined local variable or method `error' for main:Object
#=> Array
#=> (irb):42:in `irb_binding'
#=> trace_log => path:line-of-code:block ....

所以 $! 可以抓回噴出來的 Error class & 把它 puts 或是 to_s 可以取到簡易的 info,$@ 取出的是一個 trace error 的 log array,所以就算是用了 rescue 也可以知道東西錯在哪裡

再來是單行的寫法,類似

h = error rescue "yoo"
#=> "yoo"

這樣可以不理會前面的描述錯誤,且丟一個預設值回去,類似 Rails 下可以寫這樣的東西

@book = Book.find(params[:id]) rescue Book.new

也就是如果 Book 找不到該 id,則塞一個新的 Book 物件回去,而實用方式就看個人和需求之類的

再來是 method 的 rescue 縮寫

def yooo
  begin
    error
  rescue
    return 'yoo'
  end
end

#可寫成以下(同義寫法)

def yooo
  error
rescue
  return 'yoo'
end

這樣就可以少掉一層縮排還有 code ,很方便且很清楚,建議 full method的error handler 都這樣寫

再來是如何發一個錯誤回去,類似

def i_need_12(source)
  raise "QwQ not 12...(input #{source})" if source != 12
end

i_need_12(13)
#=> RuntimeError: QwQ not 12...(input 13)
#=>  ...trace log...

當然,你可以指定 Error class 並且可以自己指定

class NotNeedInputError < StandardError
end

raise NotNeedInputError , 'QwQ not 12'

#=> NotNeedInputError: QwQ not 12
#=> ...trace log...

之後就可以在上層的 rescue 進行 class 的判別之類的,而判別 Error class 可以用這樣的方式來寫( 這邊 code 多煩請下捲 )

class StrInputError  < StandardError ; end
class IntInputError   < StandardError ; end
class FloatInputError < StandardError ; end
class WTFInputError < StandardError ; end

def error_me(source)
   case source  ##此處教學用就不用變數縮寫
   when String
     raise StrInputError   , "QwQ ...str... #{source.class}#{source}"
   when Integer
     raise IntInputError   , "QwQ ...int... #{source.class}#{source}"
   when Float
     raise FloatInputError , "QwQ ...float. #{source.class}#{source}"
   else
     raise WTFInputError   , "QwQ ...wtf... #{source.class}#{source}"
   end
end

def input_me(source)
  error_me(source)
rescue StrInputError
  puts $!
rescue IntInputError
  puts $!
rescue FloatInputError => e
  puts e
rescue #此處也可以寫成 else,也就是上面都沒有match時會執行 ,不過單寫rescue比較正規
  puts $!
end

input_me('123')
#=> QwQ ...str... String123
input_me(123)
#=> QwQ ...int... Fixnum123
input_me(123.0)
#=> QwQ ...float. Float123.0
input_me([123])
#=> QwQ ...wtf... Array[123]

所以上面除了單行,都可以使用這種多個 rescue + Error class 限定來做,沒加的話就是萬用,而上面的 code

begin
rescue Error => e
end

的用法是比較好的變數宣告方是就是了,也就是全等於 $! 的使用方式,不過可以直接定義變數名稱

最後一個額外的奇怪的寫法 : ensure

begin
  error
rescue
  #do something rollback
  raise
end

#全等於
begin
  error
ensure
  #do something rollback
end

這東西可以學method連寫,rescue 和 ensure 可以同時存在,ensure 只做一件事情,也就是"當如果錯了,要用什麼補救",無法指定錯誤類型,只要有錯就會做,而如果沒有另外寫 rescue 則最後會 raise 原來的 Error 回去,也就是 method 本身可以做些事情來 “收尾” ,而非把爛攤子丟給上層呼叫

綜合以上,所有的東西可以寫成這樣子的區塊,此寫法包括method的縮寫,此部分參考於這邊

begin
  # 可能會有錯誤的code
rescue SomeExceptionClass => some_variable
  # 指定錯誤類型
rescue SomeOtherException => some_other_variable
  # 上面的指定錯誤的話可以繼續連續指定
else #同 rescue 後面沒寫東西
  # 如果類型都不是的話就跑到這邊來
ensure
  # 只要錯就會做這邊,不管類型
end

okay,錯誤處理大概就這樣而已,不外乎就是發出 / 接收,然後再來是分離與定義

至於如果感覺到 $! / $@ 這類變數它X的代表啥意思不知道的話,Ruby 的 Std lib 內有個叫做 English 的 lib,可以去看它的定義方式,類似在這邊它有翻譯好好的名稱可以看

對了,再附加一個比較深的議題,詳細不解釋太多,自己去這邊看唄

大概就是說一些注意事項 & 挑到一個對的Error class來用也是很大的重點就是了

以上 :smile:


model自訂錯誤訊息
#2

補充一下,ensure應該是"無論如何都會做一次",而非錯了才會做,舉例如下

begin
  puts "No error raised"
rescue
  puts "Rescue error"
ensure
  puts "Always do this part"
end

# 得到的結果是
No error raised
Always do this part
=> nil