We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
Phoenix contexts should return tuples, not raise
almirsarajcic
Phoenix context functions that raise exceptions break LiveView error handling and force try/catch everywhere. Tagged tuples enable composable error handling with with
statements and seamless form validation.
# ❌ EXCEPTION-THROWING - Crashes can't be composed
defmodule Accounts do
def get_user!(id) do
# Crashes on missing user
Repo.get!(User, id)
end
def create_user!(attrs) do
%User{}
|> User.changeset(attrs)
# Crashes on validation errors
|> Repo.insert!()
end
end
# Caller can't handle errors gracefully
def handle_event("save", params, socket) do
# Crashes entire LiveView!
user = Accounts.create_user!(params)
{:noreply, assign(socket, :user, user)}
end
# ✅ TAGGED TUPLES - Composable error handling
defmodule Accounts do
def get_user(id) do
case Repo.get(User, id) do
nil -> {:error, :not_found}
user -> {:ok, user}
end
end
def create_user(attrs) do
%User{}
|> User.changeset(attrs)
|> Repo.insert()
end
end
# Caller handles all outcomes naturally
def handle_event("save", params, socket) do
case Accounts.create_user(params) do
{:ok, user} ->
{:noreply, socket |> put_flash(:info, "Success!") |> assign(:user, user)}
{:error, changeset} ->
{:noreply, assign(socket, :changeset, changeset)}
end
end
Why tagged tuples win
LiveView authorization - Handle nil results for not-found/unauthorized:
def handle_params(%{"id" => id}, _uri, socket) do
case Drops.get_drop(socket.assigns.current_user, id) do
nil ->
{:noreply,
socket
|> put_flash(:error, "Drop not found")
|> push_navigate(to: ~p"/drops")}
drop ->
{:noreply, assign(socket, :drop, drop)}
end
end
For scoped queries, context functions can return nil
directly - LiveView handles it gracefully without crashes.
Composable workflows - Chain multiple operations with with
:
def transfer_money(from_id, to_id, amount) do
with {:ok, from_account} <- get_account(from_id),
{:ok, to_account} <- get_account(to_id),
{:ok, _} <- validate_balance(from_account, amount),
{:ok, from_account} <- withdraw(from_account, amount),
{:ok, to_account} <- deposit(to_account, amount) do
{:ok, {from_account, to_account}}
end
end
If any step fails, the entire chain short-circuits and returns the error tuple. No nested try/catch blocks needed.
Phoenix LiveView integration - Forms show validation errors automatically:
def handle_event("save_user", %{"user" => params}, socket) do
case Accounts.create_user(params) do
{:ok, user} ->
{:noreply,
socket
|> put_flash(:info, "User created!")
|> push_navigate(to: ~p"/users/#{user.id}")}
{:error, changeset} ->
{:noreply, assign(socket, :changeset, changeset)}
end
end
Phoenix renders changeset errors in your form without crashes or special handling.
When to keep the bang versions - Use !
functions only when you want crashes:
# Seeds that must succeed or app is broken
def seed_required_data do
admin = Accounts.create_user!(admin_attrs)
Accounts.create_role!(%{name: "admin", user_id: admin.id})
end
Pro tip: For scoped authorization queries (filtered by user/company), return nil
on not-found - LiveView handles it naturally. For operations that need detailed errors, use {:error, :reason}
tuples with with
statements.
copied to clipboard