This works most of the time
def handle(data) do
with {:ok, product} <- Jason.decode!(data),
{:ok, category} <- Category.load(data.category_id)
do
# todo
end
end
But what if you want to have more context sensitive errors. The with's else
probably doesn't have the context about which clause failed. So you need to add the context. For simple cases, you can make the code less readable with something like:
def handle(data) do
with {_, {:ok, product}} <- {:decode, Jason.decode!(data)},
{_, {:ok, category}} <- {:category, Category.load(data.category_id)}
do
# todo
else
{:decode, err} -> # handle decode error
{:category, err} -> # handle decode error
end
end
But of course, this is 1) ugly and in the {:category, err} error handler, you don't have access to product
, which you might want.
The "cleaner" solution is to have your own wraping functions
def handle(data) do
with {:ok, product} <- decode(data),
{:ok, category} <- load_category(product)
do
# todo
end
end
But then you lose the readability that inlining provides (for such simple statements) and end up with something akin to Go's 200% error handling verbosity tax.
To me, the right solution is mix of a pattern matching + guard clause + return (like in Rust):
product = case Jason.decode(data) do
{:ok, product} -> product
err ->
# whatever you need
return
end
category = case Category.load(product.category_id) do
{:ok, category} -> category
err ->
# whatever you need
return
end