I have a function work
that get’s a record, requests data from a remote api and updates that record or returns some error tuples.
It’s possible for the DB record get to fail if the id is invalid and unsure what the generally preferred way of handling that kind of error is.
Personally I prefer option 2, but I worry about its limitations as described below.
Option 1
try/rescue
first, case-match no the result of that, nest the with. Seems reasonable if a little verbose? Perhaps there is a nicer way to tie the try result with the case statement? This style reads ok here, but falls down when the with
content is actually fairly long.
def work(id) do
fruit = try do
# will raise on bad id
Fruits.get_fruit!(id)
rescue
Ecto.NoResultsError ->
{:error, :invalid_fruit_id}
end
case fruit do
{:error, e} -> {:error, e}
fruit ->
with {:ok, taste} <- TastesAPi.get_taste_for_fruit(fruit.name) do
{:ok, fruit} = Fruit.update_fruit(fruit, %{taste: taste})
else
{:error, 404} -> {:error, :no_taste_for_fruit}
{:error, :rate_limit} -> {:error, :api_rate_limited}
end
end
end
Option 2
Move everything into a with
, use a method that wont raise
, check for nil
instead.
It’s nicer to read, but how would you handle two functions that might return nil
and want to return a different error depending on which failed? (Imagine you had Fruits.get_fruit_or_nil
along with Mouth.get_mouth_or_nil
and wanted to return :invalid_fruit_id
or :invalid_mouth_id
) Maybe if your handling is that complex it’s just an indication that you need more than a compressed with
– returning to option 1?
def work(id) do
with fruit when not is_nil(fruit) <- Fruits.get_fruit_or_nil(id),
{:ok, taste} <- TastesAPI.get_taste_for_fruit(fruit.name)
do
{:ok, fruit} = Fruit.update_fruit(fruit, %{taste: taste})
else
nil -> {:error, :invalid_fruit_id}
{:error, 404} -> {:error, :no_taste_for_fruit}
{:error, :rate_limit} -> {:error, :api_rate_limited}
end
end
Option 3
try/rescue
with a throw
. Not great, means you have to wrap your caller in try/catch
. Seems like a worse version of option 1 by all metrics.
def work(id) do
fruit = try do
# will raise on bad id
Fruits.get_fruit!(id)
rescue
Ecto.NoResultsError ->
# use throw to 'return' early.
throw {:error, :invalid_fruit_id}
end
with {:ok, taste} <- TastesAPi.get_taste_for_fruit(fruit.name)
do
{:ok, fruit} = Fruit.update_fruit(fruit, %{taste: taste})
else
nil -> {:error, :inavlid_fruit_id}
{:error, 404} -> {:error, :no_taste_for_fruit}
{:error, :rate_limit} -> {:error, :api_rate_limited}
end
end