♻️ 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.