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

错误处理

Elixir 通常会返回 {:error, reason} 元组来表示错误, 但也支持产生异常. 这节课我们来看看都有那些处理错误的方式, 以及我们该怎样处理错误.

一般情况下在 Elixir 中 (example/1) 的函数会返回 {:ok, result}{:error, reason}. 另一个 (example!/1) 的函数会返回 unwrapped 的结果或者触发一个错误.

这节课我们将关注后者

约定习俗

当前,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

最常见的用法是处理需要关闭的文件和连接:

{: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 可以获取到这个值来进行使用:

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 非常少见, 通常在 library 提供的 API 不够完善时来产生缺省的错误信息.

Exiting

Elixir 中最后一个产生错误的方式是 exit. 当进程死掉的时候会产生退出信号, 它也是 Elixir 容错机制重要的一部分.

我们用 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 来捕获退出, 但这样的做法非常少见. 大多数的情况下使用 supervisor 来处理进程的退出更好:

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!