Exploring with, the Elixir special form

by Marcos Almonacid

This is my short exploration of with, the new Elixir special form introduced in v1.2.


If we check the documentation, it says that with is used to combine matching clauses. And if all the clauses match, the do block is executed, returning its result. Otherwise the chain is aborted and a non-matched value is returned:

opts = %{width: 10, height: 15}
with {:ok, width} <- Map.fetch(opts, :width),
     {:ok, height} <- Map.fetch(opts, :height),
     do: {:ok, width * height}
#=> {:ok, 150}

opts = %{width: 10}
with {:ok, width} <- Map.fetch(opts, :width),
     {:ok, height} <- Map.fetch(opts, :height),
     do: {:ok, width * height}
#=> :error

The documentation also says: variables bound inside with won’t leak, and also it allows “bare expressions”:

width = nil
opts = %{width: 10, height: 15}
with {:ok, width} <- Map.fetch(opts, :width),
     double_width = width * 2,
     {:ok, height} <- Map.fetch(opts, :height),
     do: {:ok, double_width * height}
#=> {:ok, 300}

#=> nil

Some usages

This looks cool. But when should we use it? There are some situations where using with is a good idea. I’m going to mention 2 of them.

Replacing nested case statements

Let’s say we have a function to setup a socket that listens on a specific port, accept a connection on this socket, and wait for incoming packets. We could have something like:

def server(port, opts) do
  case :gen_tcp.listen(port, opts) do
    {:ok, lsock} ->
      case :gen_tcp.accept(lsock) do
        {:ok, sock} ->
        error ->
    error ->

def do_recv(sock) do
  case :gen_tcp.recv(sock, 0) do
    {:ok, packet} ->
    {:error, :closed} ->

server/2 has 2 nested case’s. We can rewrite it using with:

def server(port, opts) do
  with {:ok, lsock} <- :gen_tcp.listen(port, opts),
       {:ok, sock}  <- :gen_tcp.accept(lsock),
       do: do_recv(sock)

This new implementation is shorter and more readable.


with can also be used to validate data before doing something with this data.

Let’s say we want to create a new user in our database by storing it in our database; but in order to do so, we have to run some validations over the data.

If we use with, we could write something like:

def create(user) do
  with :ok <- validate_name(user),
       :ok <- validate_email(user),
       :ok <- validate_token(user),
       :ok <- validate_location(user),
       do: persist(user)

The code looks pretty simple.

We run every validation before calling persist/1. If one of the validations returns something different than :ok, for example, validate_email/1 returns {:error, :invalid_email}, the chain will be aborted and create/1 will return the error tuple.

(We can implement create/1 in a few different ways. Using with is just one more)

What about the ‘Let it crash’ culture?

Ok, with is a nice special form, our code looks simple and readable when we use it. But I’m an Erlang developer, so if something doesn’t return the result that I’m expecting, the process running that code must die and its supervisor might restart it (depending on the restart strategy).

We could say that with hides MatchError crashes. So that, using with is kind of using try/catch statements. Well, I think that’s not really true. Because whatever with returns is going to be an expected result, regardless if it’s a successful response or an error response. For instance, if we call our create/1 function and it returns {:error, reason}, we might want to do something with that error (log it, send a notification, etc) before finishing the process.

case create(user) do
  {:ok, id} ->
    {:ok, id}
  {:error, reason} ->
    handle_creation_error(user, reason)


This new special form is a good tool to write simple code when it’s used in the right situation. As any other tool, trying to use it everywhere would be a mistake.

It doesn’t hide MatchError crashes, it simply has different possible returns. So it’s not breaking the 'Let it crash’ rule. For example, we know that our create/1 function returns {:ok, id} or {:error, reason}, if we only want to accept{:ok, id} we simply match the result to it:

{:ok, id} = create(user)

And as a downside, I would say that I’m starting to get a little “scared” about seeing new special forms, because they provide new ways to do something that we are already doing. And having multiple ways to do the same thing makes me remember Ruby and its “infinite” alternatives to write the same logic, which sometimes makes me feel that I’m not able to choose the right one. Anyway, Elixir doesn’t have that problem (yet). So, all in all, I can say I’m enjoying this new special form.

Go back to the blog


Thank you for your message

We sent you a confirmation email to let you know we received it. One of our colleagues will get in touch shortly.
Have a nice day!