Enum function that short circuits on first error result

I’d like to take a list of items and map them to a list of results from some function. That function might return an error, and if it does I’d like to stop processing the rest of the input and process that error and return the message. Is this possible?

mylist
|> Enum.map(fn item -> result_or_error!(item) end)

The above would raise on the error. It seems like try do result_or_error!(item)... rescue ... would work except that would return a list containing the processed error output. So I’d have to try a find or any? conditional to return the processed error output. This seems inefficient (traversing the list twice and ultimately discarding some of the work that was done unnecessarily).

You might look in the Enum module functions ending with while…

Probably You are looking for Enum.take_while.

1 Like

Enum.reduce_while/3 for the rescue.

mylist
|> Enum.reduce_while([], fn item, acc ->
  if result_or_error(item),
    do: {:cont, [item | acc]},
    else: {:halt, {:error, [item, acc]}}
end)
|> case do
  {:error, [item, collected_so_far]} -> ...
  list when is_list(list) -> Enum.reverse(list)
end
4 Likes

Another option, that is not that popular in Elixir (it is more popular in Erlang), is to use throw/1:

def foo(list) do
  for elem <- list do
    case result_or_error(elem) do
      {:ok, val} -> val
      err -> throw({:bonkers, err})
    end
  end
catch
  {:bonkers, err} -> err
end
2 Likes

@mudasobwa, that is pretty close to what I’m after. For some reason it didn’t occur to me that on the error case I could return something other than the accumulator. I guess because in Rust the iterator _while methods don’t work that way. In Rust I would use try_fold.

@hauleth, thanks! That actually reads little more nicely to me. With the Enumerable _while functions I expect the function to stop when a condition is met but to always return the accumulator. Your technique makes it more clear to me what the intent is.

Yet another option would be to use recursion:

defp process_items([]), do: {:ok, []}

defp process_items([item | rest]) do
  with {:ok, processed_item} <- result_or_error(item),
       {:ok, processed_rest} <- process_items(rest),
       do: {:ok, [processed_item | processed_rest]}
end

This approach assumes that result_or_error returns {:ok, result} | {:error, reason}

4 Likes

Is that more or less peeling back the “magic” of the Enum functions like reduce_while?

Yeah, underneath every Elixir loop is a recursion :slight_smile:

1 Like