Do you want to pick up from where you left of?
Take me there

錯誤處理

儘管常見到回傳 {:error, reason} tuple,但 Elixir 也支援異常 (exceptions),在本課程中,將看看如何處理錯誤和我們可用的不同機制。

一般來說,Elixir 中的慣例 (convention) 是建立一個函數 (example/1) 回傳 {:ok, result}{:error, reason}。而一個單獨的函數 (example!/1) 回傳無包圍的 (unwrapped) result 或觸發一個錯誤。

本課程將著重於與後者互動

一般慣例

目前,Elixir 社群已就回傳錯誤達成了一些約定:

我們慣於通過 模式比對 處理標準流程錯誤,但是在本課程中,將重點討論第二種情況 - 異常情況。

通常,在公用 API 中,也可以找到函數帶有 !(example!/1)回傳已展開結果或抛出錯誤的第二版本。

錯誤處理 (Error Handling)

在處理錯誤之前,需要建立它們,而最簡單的方法是使用 raise/1

iex> raise "Oh no!"
** (RuntimeError) Oh no!

如果要指定型別和訊息,需要使用 raise/2

iex> raise ArgumentError, message: "the argument value is invalid"
** (ArgumentError) the argument value is invalid

當知道錯誤可能發生時,可以使用 try/rescue 和模式比對來處理它:

iex> try do
...>   raise "Oh no!"
...> rescue
...>   e in RuntimeError -> IO.puts("An error occurred: " <> e.message)
...> end
An error occurred: Oh no!
:ok

在一個單獨的 rescue 中是可能配對多個錯誤:

try do
  opts
  |> Keyword.fetch!(:source_file)
  |> File.read!()
rescue
  e in KeyError -> IO.puts("missing :source_file option")
  e in File.Error -> IO.puts("unable to read source file")
end

After

有時可能需要在 try/rescue 之後執行一些操作,而忽略錯誤訊息。為此可以使用 try/after 。如果熟悉 Ruby,則類似於 begin/rescue/ensure 或 Java 中的 try/catch/finally

iex> try do
...>   raise "Oh no!"
...> rescue
...>   e in RuntimeError -> IO.puts("An error occurred: " <> e.message)
...> after
...>   IO.puts "The end!"
...> end
An error occurred: Oh no!
The end!
:ok

最常用在應該關閉的檔案或連結 (connections):

{:ok, file} = File.open("example.json")

try do
  # Do hazardous work
after
  File.close(file)
end

New Errors

雖然 Elixir 包含許多類似於 RuntimeError 的內建錯誤型別,但是如果需要特定的東西,仍然可以建立自己的錯誤型別。使用 defexception/1 巨集且用 :message 選項設定一個預設的錯誤訊息就能很容易產生一個新的錯誤訊息:

defmodule ExampleError do
  defexception message: "an example error has occurred"
end

現在來看看新的錯誤訊息:

iex> try do
...>   raise ExampleError
...> rescue
...>   e in ExampleError -> e
...> end
%ExampleError{message: "an example error has occurred"}

Throws

在 Elixir 中處理錯誤的另一個機制是 throwcatch。在實踐中,這些錯誤在新的 Elixir 程式碼很少發生,但知道並理解它們仍是重要的。

throw/1 函數使我們能夠用一個能 catch 並使用的特定值來退出 (exit) 執行:

iex> try do
...>   for x <- 0..10 do
...>     if x == 5, do: throw(x)
...>     IO.puts(x)
...>   end
...> catch
...>   x -> "Caught: #{x}"
...> end
0
1
2
3
4
"Caught: 5"

如前所述, throw/catch 是非常不常見的,當函數庫無法提供足夠的 API 時,通常會以暫時的形式存在。

Exiting

Elixir 提供的最後一個錯誤機制是 exit。Exit 信號發生在一個程序死亡時,這是 Elixir 故障容錯 (fault tolerance) 的重要組成部分。

要明確地 exit,可以使用 exit/1

iex> spawn_link fn -> exit("oh no") end
** (EXIT from #PID<0.101.0>) evaluator process exited with reason: "oh no"

儘管可以用 try/catch 來 catch 一個 exit,但這樣做是 極端 罕見的。因為在幾乎所有情況下,讓 supervisor 處理 exit 程序是有利的:

iex> try do
...>   exit "oh no!"
...> catch
...>   :exit, _ -> "exit blocked"
...> end
"exit blocked"
Caught a mistake or want to contribute to the lesson? Edit this lesson on GitHub!