Yes, finally! We now have in Elixir the with keyword! (See what I did there with the post title?)

In a gist, with allows you to sequence function calls and return a certain value, unless one of the functions returns something else.

But what does that actually mean? Allow me to explain with a real life sample of something that was a pain to deal with previously: sequential flows.

Let’s say you have a User record that you want to insert in your database, but you want to make sure that:

  • Values that have been provided are correct
  • The record is inserted in the database
  • An e-mail is sent afterwards.

Given there is validation, database operation and communication with external services, things can go sideways. Before with you’d likely have to do something like:

def register_user(name, email) do
validate_email(email)
|> create_user(name)
|> send_email
end
defp validate_email(email) do
# returns {:ok, email} when email is valid
# or {:error, error_message} otherwise
end
defp create_user({:error, error_message}, _name) do
{:error, error_message}
end
defp create_user({:ok, email}, name) do
# creates user on DB and returns {:ok, user} on success
# or {:error, error_message} on failure
end
defp send_email({:ok, user}) do
# returns {:ok} or {:error, error_message}
end
defp send_email({:error, message}) do
{:error, message}
end

Having the code layout like this allows us to keep a clean public API function, but the private functions always have to take into account that an {:error, …} can be received as an argument. Variations on this exist using cases or ifs.

Not pretty and highly coupling: the definition of a function depends on the different outputs form other.

Enter with!

def register_user(name, email) do
with {:ok} <- validate_email(email),
{:ok, user} <- create_user(email, name),
{:ok} <- send_email(user),
do: {:ok, user, "foobar"}
end
defp validate_email(email) do
# Validates the user e-mail and returns {:ok} when valid
# or {:error, error_message} when invalid
end
defp create_user(email, name) do
# creates user on DB and returns {:ok, user} on success
# or {:error, error_message} on failure
end
defp send_email(user) do
# sends an e-mail to the user and returns {:ok} on success
# or {:error, error_message} on failure
end

The beauty of with is that we now don’t have to worry about taking special care of error cases. As long as the function on the right of <- successfuly pattern matches the definition on the left of <-, the next block will be executed.

At the end, the do block is returned. In the case above, that’d be {:ok, user, “foobar”}.

If, for example, create_user/1 returned {:error, [:id, “ primary key violation”]}, then this would be the value returning from with and send_email would never be executed. Awesomeness!

Hope I helped shedding some light to with, here are the official docs for good measure.

Check my other Elixir posts here.

Thanks for reading!