You can raise an exception using the raise function. At its simplest, you pass it a string and it generates an exception of type RuntimeError.
| iex> raise "Giving up" |
| ** (RuntimeError) Giving up |
| erl_eval.erl:572: :erl_eval.do_apply/6 |
You can also pass the type of the exception, along with other optional fields. All exceptions implement at least the message field.
| iex> raise RuntimeError |
| ** (RuntimeError) runtime error |
| erl_eval.erl:572: :erl_eval.do_apply/6 |
| iex> raise RuntimeError, message: "override message" |
| ** (RuntimeError) override message |
| erl_eval.erl:572: :erl_eval.do_apply/6 |
You can intercept exceptions using the try function. It takes a block of code to execute, and optional rescue, catch, and after clauses.
The rescue and catch clauses look a bit like the body of a case function—they take patterns and code to execute if the pattern matches. The subject of the pattern is the exception that was raised.
Here’s an example of exception handling in action. We define a module that has a public function, start. It calls a different helper function depending on the value of its parameter. With 0, it runs smoothly. With 1, 2, or 3, it causes the VM to raise an error, which we catch and report.
| defmodule Boom do |
| def start(n) do |
| try do |
| raise_error(n) |
| rescue |
| [ FunctionClauseError, RuntimeError ] -> |
| IO.puts "no function match or runtime error" |
| error in [ArithmeticError] -> |
| IO.inspect error |
| IO.puts "Uh-oh! Arithmetic error" |
| reraise "too late, we're doomed", System.stacktrace |
| other_errors -> |
| IO.puts "Disaster! #{inspect other_errors}" |
| after |
| IO.puts "DONE!" |
| end |
| end |
| |
| defp raise_error(0) do |
| IO.puts "No error" |
| end |
| |
| defp raise_error(val = 1) do |
| IO.puts "About to divide by zero" |
| 1 / (val-1) |
| end |
| |
| defp raise_error(2) do |
| IO.puts "About to call a function that doesn't exist" |
| raise_error(99) |
| end |
| |
| defp raise_error(3) do |
| IO.puts "About to try creating a directory with no permission" |
| File.mkdir!("/not_allowed") |
| end |
| end |
We define three different exception patterns. The first matches one of the two exceptions, FunctionClauseError or RuntimeError. The second matches an ArithmeticError and stores the exception value in the variable error. And the last clause catches any exception into the variable other_error.
We also include an after clause. This will always run at the end of the try function, regardless of whether an exception was raised.
Finally, look at the handling of ArithmeticError. As well as reporting the error, we call reraise. This raises the current exception, but lets us add a message. We also pass in the stack trace (which is actually the stack trace at the point the original exception was raised). Let’s see all this in IEx:
| iex> c("exception.ex") |
| [Boom] |
| iex> Boom.start 1 |
| About to divide by zero |
| %ArithmeticError{} |
| Uh-oh! Arithmetic error |
| DONE! |
| ** (RuntimeError) too late, we're doomed |
| exception.ex:26: Boom.raise_error/1 |
| exception.ex:5: Boom.start/1 |
| |
| iex> Boom.start 2 |
| About to call a function that doesn't exist |
| no function match or runtime error |
| DONE! |
| :ok |
| |
| iex> Boom.start 3 |
| About to try creating a directory with no permission |
| Disaster! %File.Error{action: "make directory", path: "/not_allowed", |
| reason: :eacces} |
| DONE! |
| :ok |