One of my favorite language constructs of elixir is the pipe operator (|>). It makes really readable code:
defmodule User do
def set_avatar(args) do
args
|> upload_to_s3
|> create_responsive_sizes
|> save_to_db
end
end
It's easy to tell by reading the function set_avatar
what is going on here without needing to see the other functions.
How do you handle errors in this scenario? There are a few options available:
with
statementLet's look at the third option: with
. Consider this example:
defmodule User do
def set_avatar(args) do
with {:ok, url} <- upload_to_s3(args),
{:ok, sizes} <- create_responsive_sizes(url),
{:ok, result} <- save_to_db(sizes) do
# Return the result
{:ok, result}
else
# Handle specific errors here
{:error, :network_timeout} ->
set_avatar(args, retries: 1)
error ->
Logger.error("Could not set sizes: #{IO.inspect(error)}")
{:error, :could_not_set_avatar}
end
end
end
The with
statement matches the return of each function. If they all match, the first do
block is executed:
defmodule User do
def set_avatar(args) do
with {:ok, url} <- upload_to_s3(args),
{:ok, sizes} <- create_responsive_sizes(url),
{:ok, result} <- save_to_db(sizes) do
# Return the result
{:ok, result}
else
# Handle specific errors here
{:error, :network_timeout} ->
set_avatar(args, retries: 1)
error ->
Logger.error("Could not set sizes: #{IO.inspect(error)}")
{:error, :could_not_set_avatar}
end
end
end
If any of the returns do not match, the else block gets executed:
defmodule User do
def set_avatar(args) do
with {:ok, url} <- upload_to_s3(args),
{:ok, sizes} <- create_responsive_sizes(url),
{:ok, result} <- save_to_db(sizes) do
# Return the result
{:ok, result}
else
# Handle specific errors here
{:error, :network_timeout} ->
set_avatar(args, retries: 1)
error ->
Logger.error("Could not set sizes: #{IO.inspect(error)}")
{:error, :could_not_set_avatar}
end
end
end
In the else block you can handle specific errors by matching on function returns. Here we handle a :network_timeout
specifically and any other error generically.
defmodule User do
def set_avatar(args) do
with {:ok, url} <- upload_to_s3(args),
{:ok, sizes} <- create_responsive_sizes(url),
{:ok, result} <- save_to_db(sizes) do
# Return the result
{:ok, result}
else
# Handle specific errors here
{:error, :network_timeout} ->
set_avatar(args, retries: 1)
error ->
Logger.error("Could not set sizes: #{IO.inspect(error)}")
{:error, :could_not_set_avatar}
end
end
end
Another helpful feature of the with
statement is the ability to use results returned by each function in the next. For example, if our save_to_db
function needed the url
and sizes
we could do that with:
defmodule User do
def set_avatar(args) do
with {:ok, url} <- upload_to_s3(args),
{:ok, sizes} <- create_responsive_sizes(url),
{:ok, result} <- save_to_db(sizes, url) do
# Return the result
{:ok, result}
else
# Handle specific errors here
{:error, :network_timeout} ->
set_avatar(args, retries: 1)
error ->
Logger.error("Could not set sizes: #{IO.inspect(error)}")
{:error, :could_not_set_avatar}
end
end
end
I have found the with
statement quite useful as I develop applications with elixir. I hope this example helps!