Skip to content

Elixir With

William Schroeder edited this page Oct 21, 2021 · 3 revisions

Elixir's with expression offers an interesting twist on error handling.

Suppose we are parsing ElasticSearch results with this simplified and converted result:

result = %{
  hits: %{
    total: 1,
    hits: [
      %{
        _source: %{
          my_document: %{
            name: "Example",
            buried_json: "{\"color\": \"red\"}"
          }
        }
      }
    ]
  }
}

If we expect the result to always have exactly one hit and that hit always has the same structure, we might use a bit of Intentional Programming and let it crash and log the internal error for us:

{:ok, buried_json}         = get_buried_json(result)
{:ok, %{"color" => color}} = Jason.decode(buried_json)
{:ok, hex}                 = color_to_hex(color)
hex   # return this

However, we may expect to be parsing user data and need to return a useful message to the user. The typical with approach I see in searches would translate the above example to this expression:

with {:ok, buried_json}         <- get_buried_json(result),
     {:ok, %{"color" => color}} <- Jason.decode(buried_json),
     {:ok, hex}                 <- color_to_hex(color) do
  {:ok, "Color code is: #{hex}"}   # return this
else
  {:error, message} -> {:failed, message}
end

This is similar in feel to try/catch expressions or error pipelines in other languages, but the approach suffers the same problem those other languages do: which of those three parsing functions resulted in a Jason.DecodeError? Which fell through to the more generic error form?

The solution I came up with recently is to tag, or type, each call:

with {_, {:ok, buried_json}}         <- {:get_buried,    get_buried_json(result)},
     {_, {:ok, %{"color" => color}}} <- {:decode_buried, Jason.decode(buried_json)},
     {_, {:ok, hex}}                 <- {:hexed,         color_to_hex(color)} do
  {:ok, "Color code is: #{hex}"}   # return this
else
  {:get_buried,    {:error, message}} -> {:failed_to_get_buried,      message}
  {:decode_buried, {:error, message}} -> {:failed_to_decode_buried,   message}
  {:hexed,         {:error, message}} -> {:failed_to_get_hexed_color, message}
end

Clone this wiki locally