Railway development in Elixir using `with`
A year ago, I came across the great post by Zohaib Rauf on Railway programming in Elixir. Here was the code Zohaib used as an example on the post:
request
|> validate_request
|> get_user
|> update_db
|> send_email
|> return_http_message
See the problem? What if get_user
, update_db
, or send_email
fails? How do we handle the error?
Here's one of the great illustration by Zohaib to show the problem above:
Picture from Zohaib Rauf from this blog post
Handle success and errors with with
Fortunately, in Elixir 1.2, we get the with
keyword. The with keyword lets us handle these scenarios with ease. Hat tip to trenpixster for the great blog post which got me thinking about this.
Let's return to the example above and use the with keyword
def handle_request(request) do
with {:ok} <- validate_request(request),
{:ok, user} <- get_user(request),
{:ok} <- update_db(user),
{:ok} <- send_email(user) do
return_http_message
else
{:error, reason} -> handle_error(reason)
_ -> handle_ambigous_error
# alternately, you could handle the errors
# {:error, :update_db, details} -> handle_update_db_error(details)
# {:error, :send_email, details} -> handle_send_email_error(details)
end
end
Let's explore what's going on: the with block has 4 expressions. Each one will execute and the return will be matched. So, the output of validate_request
will be matched against the 1-tuple {:ok}
. If the value is {:ok}
, get_user(request)
will be executed. If not, we'll go to our else
block.
[EDIT] It's not until Elixir 1.3 that we get the else
block. Everything except the else block works in Elixir 1.2. Thanks @fish0398 for the catch.
Let's look at what we get by using the with
:
- Obvious business logic It's really easy to see the happy path and what the method is trying to do.
- Obvious error handling All the possible error states are specified and easy to understand.
If you find yourself with some code that needs to have multiple outputs, try using the with
keyword to clean it up.