I’m trying to figure out the best way to handle errors in chained function calls without generating a runtime error/crash. For example, I might want to verify certain fields are present in a map, then verify the user has permissions to do something, then do something that could succeed or fail, and finally return the success or failure message.
my_map
|> verify_fields()
|> verify_permissions()
|> perform_request()
def verify_fields(%{required_field: value} = arg) do
{:ok, arg}
end
def verify_fields(arg) do
{:error, message: "Missing required field."}
end
def verify_permissions({:ok, %{user: %User{} = user} = arg}) do
case user.permission do
"allowed" -> {:ok, arg}
_ -> {:error, message: "User not permitted to do that."}
end
end
def verify_permissions({:error, msg} = err) do
err
end
def perform_request({:ok, arg}) do
#stuff that returns {:ok, result}
end
def perform_request({:error, msg} = err) do
err
end
Is there a better way to propagate errors or even to short circuit all subsequent function calls in a pipe? This approach feels cluttered and difficult to maintain.
Consider using the with form for cases like this. Your example would be written as:
with {:ok, valid_fields} <- verify_fields(my_map),
:ok <- verify_permissions(valid_fields),
{:ok, result} <- perform_request(valid_fields) do
{:ok, result}
end
def verify_fields(...) do
...same as before...
end
def verify_permissions(%{user: %User{} = user}) do
case user.permission do
"allowed" -> :ok
_ -> {:error, message: "User not permitted to do that"}
end
end
def perform_request(arg) do
...
end
As a side-effect, the functions used here have a cleaner API - they take what they need, not {:ok, ...} | {:error, term()}.
As @al2o3cr said I definitely think using with along with an else clause is the way to go when the functions in the pipeline could have an error condition.
Thank you everyone for the help! Using with seems the way to go. The linked thread of “Should one use pipes or with?” is my exact question but you would not find it with the search function unless you already know the with pattern. The discussion in that thread is very insightful.
When would the try-rescue-catch syntax be a better fit?
The ROP package looks nice but not updated in a few years. Are there active ROP style libraries? Sage also implements an approach that looks quite nice at first glance. I’m not sure how the “saga” pattern differs from the railways pattern. For my relatively simple case I’m going to stick with, well, with, but if my chains get longer than 4 or 5 functions then Sage might be worth a try.
Sage is mostly to be used if you need a transaction-like functionality that extends beyond just your DB.
An example might be: (1) validate form parameters, (2) call 3rd party API, (3) validate its response, (4) normalize the data, (5) store to your own DB, etc.
with can do the same but it can get a bit tedious to write with more steps (as you correctly are intuiting).