Error Handling
Elixir error handling with ok/error tuples, exceptions, try/rescue, and custom errors
Elixir favors explicit error handling with {:ok, value} and {:error, reason} tuples over exceptions. Exceptions are reserved for truly unexpected situations.
The ok/error pattern
# Functions return tagged tuples
case File.read("config.txt") do
{:ok, contents} -> process(contents)
{:error, :enoent} -> IO.puts("File not found")
{:error, reason} -> IO.puts("Error: #{inspect(reason)}")
end
# With can chain ok/error tuples
with {:ok, user} <- fetch_user(id),
{:ok, token} <- create_token(user),
{:ok, session} <- create_session(token) do
{:ok, session}
else
{:error, :not_found} -> {:error, "User not found"}
error -> error # pass through any other error
endBang (!) functions
Functions ending in ! raise on error instead of returning a tuple:
# Tuple version (safe)
File.read("missing.txt") # => {:error, :enoent}
# Bang version (raises)
File.read!("missing.txt") # => ** (File.Error) could not read file
# Other examples:
Map.fetch(map, :key) # => {:ok, value} | :error
Map.fetch!(map, :key) # => value | raises KeyError
Access.list_key(list, 0) # => {:ok, value} | :error
Access.list_key!(list, 0) # => value | raisesRaising exceptions
# With a message
raise "Something went wrong"
# With a specific error type
raise ArgumentError, "Invalid input: #{input}"
# Reraise (preserve original stacktrace)
try do
risky()
rescue
e -> reraise e, __STACKTRACE__
endCustom exceptions
defmodule MyApp.HttpError do
defexception [:message, :status_code]
@impl true
def message(%{status_code: code, message: msg}) do
"HTTP #{code}: #{msg}"
end
end
# Raising custom exception
raise MyApp.HttpError, status_code: 404, message: "Not found"try / rescue / after
result = try do
dangerous_operation()
rescue
e in ArgumentError ->
{:error, e.message}
e in [File.Error, RuntimeError] ->
{:error, "File or runtime error: #{inspect(e)}"}
e ->
# Catch-all (use sparingly)
{:error, "Unexpected: #{inspect(e)}"}
after
# Always runs, regardless of success or failure
cleanup()
endtry / catch
For catching throw values (rare, used for control flow):
try do
Enum.each(items, fn item ->
if item == :stop, do: throw(:stopped)
process(item)
end)
:all_processed
catch
:throw, :stopped -> :stopped_early
endtry / else
Pattern match on the result of a try block:
try do
{:ok, result}
else
{:ok, value} -> value
{:error, reason} -> handle_error(reason)
endCommon error types
| Exception | When |
|---|---|
ArgumentError |
Invalid argument |
ArithmeticError |
Bad math (e.g., div by zero) |
KeyError |
Key not found in map/keyword |
Enum.OutOfBoundsError |
Invalid index |
File.Error |
Filesystem error |
RuntimeError |
Generic error (raise "msg") |
FunctionClauseError |
No matching function clause |
MatchError |
Pattern match failure |
CaseClauseError |
No matching case clause |
CondClauseError |
No truthy cond condition |
TryClauseError |
No matching try/else clause |
ErlangError |
Raw Erlang error |
Best practices
- Return
{:ok, value}or{:error, reason}for expected failures (file not found, validation errors, etc.) - Use exceptions for unexpected failures (programming errors, corrupt data)
- Use
withto chain ok/error tuples instead of nestedcase - Always include a catch-all clause in
caseandwithduring development - Use bang (
!) functions only when you want to crash on failure (e.g., config files that must exist) - Use
@specto document return types (@spec fetch(id :: pos_integer()) :: {:ok, User.t()} | {:error, :not_found})
The “Happy Path” pattern
# Instead of nested case:
case fetch_user(id) do
{:ok, user} ->
case create_token(user) do
{:ok, token} ->
case send_email(user, token) do
{:ok, _} -> {:ok, user}
{:error, reason} -> {:error, reason}
end
{:error, reason} -> {:error, reason}
end
{:error, reason} -> {:error, reason}
end
# Use with:
with {:ok, user} <- fetch_user(id),
{:ok, token} <- create_token(user),
{:ok, _} <- send_email(user, token) do
{:ok, user}
end
# Any {:error, reason} automatically returns {:error, reason}