Error handling
Matching Elixir conventions, Tuist server functions should return success and errors as values in form of {:ok, result}
and {:error, reason}
tuples.
In order for us to leverage automatic error handling, we follow a standardized error format that is used across both domain and interface logic.
Error format
- Any successful function execution should return
{:ok, result}
, whereresult
can be any value. - Any error where the function execution is not successful should return
{:error, reason}
, wherereason
is a string describing the error.
For generic errors, the reason
should be an atom describing the error code:
:unauthenticated
- There is no authenticated user.:unauthorized
- The authenticated user is not authorized to perform the requested action.:not_found
- The requested resource was not found.
NOTE
In our controller error handling, :unauthenticated
maps to HTTP error 401
and :unauthorized
maps to HTTP error 403
. This does not match the HTTP status code name, but matches the cause of the error more closely.
For input errors, the preference is to return an %Ecto.Changeset{}
containing the error messages pertaining to the related input fields.
For all other errors, reason
should be a string describing the error.
These error formats allow us to handle errors in a consistent way across domain logic and controllers, making it possible to simply return the errors inside our controller actions and have automatic formatting and status code handling.
Controller example
with {:ok, account} <- get_account(handle), # returns {:ok, account} or {:error, :not_found}
:ok <- can(conn.assigns.current_user, :update, account, :organization), # returns :ok or {:error, :unauthorized}
{:ok, account} <- Accounts.update_account(account, params) do # returns {:ok, account} or {:error, changeset}
conn
|> put_status(:ok)
|> json(%{id: account.id, handle: account.name})
end
Since the with
clause returns all non-matching values as-is, the error paths will be passed onto the controller which will handle them generically.