Code isn't always successful, but we haven't yet covered errors or exceptions in detail. Let's rectify that.

Iex

We'll explore exceptions using iex, so go ahead and open it.

iex

We've seen exceptions thrown before from simple errors in our code.

1 / 0
# ** (ArithmeticError) bad argument in arithmetic expression
#     :erlang./(1, 0)

You can also raise an error using raise/2:

raise ArithmeticError, message: "nope.jpg"
# ** (ArithmeticError) nope.jpg

If you omit the error name, a RuntimeError will be raised:

raise "some runtime error"
# ** (RuntimeError) some runtime error

You can define a custom error with defexception:

defexception ElixirSipError, message: "No such ElixirSip"
#=> nil

Then you can raise that error:

raise ElixirSipError
# ** (ElixirSipError) No such ElixirSip
raise ElixirSipError, message: "can't hold all these limes"
# ** (ElixirSipError) can't hold all these limes

You can use try/rescue to rescue an exception:

try do
  raise ElixirSipError
rescue
  e in ElixirSipError ->
    IO.puts "Caught an error: #{inspect e}"
end

But just because you can do a thing doesn't mean you should do that thing. try/catch is actually rarely used in Elixir code, because idiomatic Elixir will return a tuple containing information on the outcome of a given function, and this tuple can then be pattern matched on.

You'll recall seeing an {:error, :enoent} tuple returned from calls to File.read for nonexistent files. This is because it's entirely possible that you don't consider :enoent to be an exceptional case in your codebase, and this way you aren't forced to use exceptions for control flow.

If you want to follow the standard library's lead, you'll typically return informational tuples from functions, and if you want to provide exceptions you can define a function with a bang on the end. We'll define a function like that quickly:

defmodule Errors do
  def low_random do
    case :random.uniform do
      x when x < 0.5 -> {:ok, x}
      x -> {:error, :etoohigh, x}
    end
  end

  # Then, if you wanted to provide an exception-raising variant of this function
  # in your API, you could just wrap that function:
  def low_random! do
    case low_random do
      {:ok, x} -> x
      {:error, :etoohigh, x} -> raise ElixirSipError, message: "#{x} was too high, sorry"
    end
  end
end

Now we can just see the difference in these two:

Errors.low_random
Errors.low_random
Errors.low_random
Errors.low_random
Errors.low_random!
Errors.low_random!
Errors.low_random!
Errors.low_random!

Summary

That wraps up our coverage of exceptions in Elixir. See you soon!

Resources