♻️ Refactoring complex `else` clauses in `with` (an anti-pattern)

Notes

Elixir’s with clauses are awesome for organizing happy paths. But sometimes the else clauses get really messy and difficult to understand.

Elixir’s docs call this the Complex else clauses in with anti-pattern, and they offer a nice refactoring.

Suppose we have the following code:

def open_decoded_file(path) do
  with {:ok, encoded} <- File.read(path),
       {:ok, decoded} <- Base.decode64(encoded) do
    {:ok, String.trim(decoded)}
  else
    {:error, _} -> {:error, :badfile}
    :error -> {:error, :badencoding}
  end
end

Just by looking at that, it’s tough to figure out which error in the else clause belongs to which statement in the with clause.

In fact, if you imagine having more statements in the with side, you could easily see us having errors overlapping!

So, what should we do?

We can refactor to extract helper functions:

def open_decoded_file(path) do
  with {:ok, encoded} <- read_file(path),
       {:ok, decoded} <- decode(encoded) do
    {:ok, String.trim(decoded)}
  end
end

defp read_file(path) do
  case File.read(path) do
    {:ok, contents} -> {:ok, contents}
    {:error, _} -> {:error, :badfile}
  end
end

defp decode(contents) do
  case Base.decode64(contents) do
    {:ok, decoded} -> {:ok, decoded}
    :error -> {:error, :badencoding}
  end
end

Our refactoring keeps the errors close to their source, and our errors now all abide by an API (of sorts). They all return {:error, reason}. That allows our main body of open_decoded_file/1 to focus solely on the happy path! And that… well… that makes developers happy.

Want the latest Elixir Streams in your inbox?

    No spam. Unsubscribe any time.