I’m having troubles to deal with not having early returns in elixir. I read some posts and articles and still cant understand how to workaround this in a proper way. Looks like with and guards are the most used but I couldnt quite understand or apply it, can someone provide me a example?
defmodule Utils do
def get_solAmount_basedOn_pixFee(pixfee) do
price_result = case JupPriceApi.get_price_byAddress("So11111111111111111111111111111111111111112") do
{:ok, nil} ->
{:error, "empty data field"}
{:ok, token_price} ->
token_price
_ ->
{:error, "failed to get data price"}
end
usd_amount = case UsdBrl.get_USD_amount_basedOn_BRL(pixfee) do
{:ok, amount} ->
amount
_ ->
nil
end
sol_price = Decimal.from_float(price_result)
|>Decimal.round(3,:up)
|>Decimal.new()
ratio = Decimal.new(1)
usds = Decimal.from_float(usd_amount)
sols = Decimal.div(ratio, sol_price)
|> Decimal.mult(usds)
|>Decimal.round(4, :up)
IO.inspect(sols)
end
end
I have this code that fetch data from 2 api’s, what I would like to do it’s to be able to return tuples with :error and a error message. I started to write it and just now I realised that even with the case statements, i’m not actually returning and stopping the code
I asked Gemini to produce a solution, and sounds like just a fancy ifelse? and did not even checked the things I wanted
defmodule Utils do
def get_solAmount_basedOn_pixFee(pixfee) do
# Early return for invalid price data
with {:ok, token_price} <- JupPriceApi.get_price_by_address("So11111111111111111111111111111111111111112"),
{:ok, usd_amount} <- UsdBrl.get_USD_amount_basedOn_BRL(pixfee) do
# Calculate SOL amount using valid data
sol_price = Decimal.from_float(token_price) |> Decimal.round(3, :up)
ratio = Decimal.new(1)
usds = Decimal.from_float(usd_amount)
sols = Decimal.div(ratio, sol_price) |> Decimal.mult(usds) |> Decimal.round(4, :up)
IO.inspect(sols)
else
# Handle errors from JupPriceApi or UsdBrl
{:error, reason} ->
IO.inspect("Error: #{reason}")
_ ->
IO.inspect("Unexpected error")
end
end
end
If all clauses match, the do block is executed, returning its result. Otherwise the chain is aborted and the non-matched value is returned:
Thus if any of the 2 operation fails then the do block is not executed. The only problem with this is that your functions need to return the same error pattern since else can use only 1 pattern.
If JupPriceApi.get_price_by_address returns {:error, message} and UsdBrl.get_USD_amount_basedOn_BRL returns :error then this won’t work
Not true, you can match any error shape in else clause, the question is whether should you?
To understand how to use these constructs effectively, you need to understand how to stop writing defensive code. I am more than positive this was discussed already multiple times in similar topics, so you can try to search for them on forum.
The with construct follows the ideology that we chain operations together and short-circuit on the first error. This avoids what you are literally showing in your example, where you don’t early return the first error from the function but proceed with invalid state.
It is also important to understand that handling of errors in such cases happen because we expect them, if an error puts your system in an invalid state, that error should not be handled and instead you should let it crash.
def get_solAmount_basedOn_pixFee(pixfee) do
with {:ok, token_price} <- get_price(),
{:ok, usd_amount} <- get_usd_amount(pixfee) do
result =
Decimal.new(1)
|> Decimal.div(token_price)
|> Decimal.mult(usd_amount)
|> Decimal.round(4, :up)
{:ok, result}
end
end
defp get_price do
case JupPriceApi.get_price_byAddress("So11111111111111111111111111111111111111112") do
{:ok, nil} ->
{:error, "empty data field"}
{:ok, token_price} ->
token_price =
token_price
|> Decimal.from_float()
|> Decimal.round(3,:up)
{:ok, token_price}
_ ->
{:error, "failed to get data price"}
end
end
defp get_usd_amount(pixfee) do
case UsdBrl.get_USD_amount_basedOn_BRL(pixfee) do
{:ok, amount} -> {:ok, Decimal.from_float(amount)}
_ -> {:error, "failed to get USD amount"}
end
end
Here’s an example of a with statement with an inline is_nil guard.
def get_solAmount_basedOn_pixFee(pixfee) do
with {:ok, token_price} when !is_nil(token_price) <- JupPriceApi.get_price_byAddress("So1..."),
{:ok, usd_amount} <- UsdBrl.get_USD_amount_basedOn_BRL(pixfee) do
...
end
end
That said, something to consider is whether JupPriceApi.get_price_byAddress/1 should directly return {:error, "empty data field"} rather than {:ok, nil} and whether it’s useful handling it separately.