Error handling in piped functions

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()}.

3 Likes

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.

Here is a link to a good thread about it: Should one use pipes or with? - #4 by tme_317

1 Like

You may also find some interesting ideas in this repo:

It basically creates a recipe for the computations(like you would with a stream) and ultimately run it. It’s also a composable version of with.

2 Likes

did someone say railway?
I think this is the first thing I’ve ever seen about functional programming:

3 Likes

I suggested the name precisely because of that :smiley:

1 Like

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?

1 Like

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).