jonathan-scholbach

jonathan-scholbach

Avoiding long `with` blocks

I find myself using a lot of very long with-statements. The functions in the with pipeline return {:ok, result} or some {:error, :error_type, additional_error_information} It could look somewhat like this:

with \
   {:ok, result_a} <- function_a(input),
   {:ok, result_b} <- function_b(result_a),
   {:ok, result_c} <- function_c(result_b),
   :ok <- some_validation_function_d(result_c)
do
  {:ok, result_c}
else
  {:error, :error_in_a} -> {:error, :internal_error}
  {:error, :another_error_in_a} -> {:error, :some_other_error}
  {:error, :error_in_b} -> {:error, :strange_error}
  {:error, :error_in_b, msg} -> {:error, :strange_error, msg}
  {:error, :error_in_c, msg} -> {:error, :internal_error}
  {:error, :validation_error, _msg} -> {:error, :validation_error, input, msg}
end

But sometimes I have blocks that are way longer even.

This looks like a legacy of thinking like an imperative programmer to me, using a a (not so) nicely nested catch-try tree.

I would like to find a way to make my pipeline functions perform an “early return”, so I would like to “break” the pipeline.

It should look something like this instead (not working pseudocode following):

input
|> {function_a(), error_handler_a}
|> {function_b(), error_handler_b}
|> {function_c(), error_handler_c}
|> {function_d(), error_handler_d}

Where each error_handler maps the error from the function to the correct error return I am using in this context. How do I do this best? I would be willing to write a macro (although I have no experience with this.) Another constraint: I do not want to use an external dependency for this, but be in full control of the code myself.

I am new to Elixir, and I can imagine that my idea of a solution does not make too much sense. If this is the case, what are other best practices to avoid the problem in the first place?

Most Liked

stefanchrobot

stefanchrobot

One way to approach this is to avoid the else clause in with. This means that you need to introduce a somewhat generic error struct - it can be app-wide, or specific to some context. All the smaller functions will need to return the error struct on error or you’ll need to write local wrappers around them. So it would be something like this:

# Generic error.
defmodule MyApp.Error do
  defexception [:code, :message, :meta]

  @impl Exception
  def message(%__MODULE__{code: code, message: message, meta: meta}) do
    "Error (#{code}): #{message}, meta: #{inspect(meta)}"
  end

  def new(code, message, opts \\ []) do
    %__MODULE__{
      code: code,
      message: message,
      meta: opts[:meta] || %{}
    }
  end

  def auth_error() do
    new(:auth_error, "authorization error")
  end

  def validation_error(changeset) do
    new(:validation_error, "validation error", meta: changeset)
  end
end

# App logic.
defmodule MyApp.Foo do
  def bar(input) do
    with {:ok, result_a} <- function_a(input),
         {:ok, result_b} <- function_b(result_a),
         {:ok, result_c} <- function_c(result_b),
         :ok <- some_validation_function_d(result_c) do
      {:ok, result_c}
    end
  end

  # Example of wrapping.
  defp some_validation_function_d(result_c) do
    case run_validation(result_c) do
      :ok -> :ok
      {:error, changeset} -> {:error, MyApp.Error.validation_error(changeset)}
    end
  end
end

# Top-level code that runs the logic (e.g. controller, background job, etc.)
defmodule MyAppWeb.FooController do
  def create(conn, _params) do
    case MyApp.Foo.bar(params) do
      {:ok, result} -> #...
      {:error, %MyApp.Error{code: :validation_error}} -> # ...
    end
  end
end

See also Good and Bad Elixir.

As an added bonus, all the functions that return {:error, %MyApp.Error{}} now became nicely composable.

Where Next?

Popular in Questions Top

sen
Hi All, I set a environment variables in dev.exs , like below code. when i start server, how can i set the ${enable} value? thanks. d...
New
_russellb
I want to try my hand at web scraping. What tools/libraries do I need to use. I’m hoping to turn this into something professional so don’...
New
vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID&lt;0.412.0&gt; terminating ** (Postgrex.Error) FATAL...
New
LegitStack
I’m trying to make a websocket server in Phoenix or raw Elixir. I heard about gun, I think I could use cowboy, but since I’m not that sma...
New
dblack
I’ve got an issue with an app and I’ve no idea of how to troubleshoot it. I’m hoping someone here might have seen something similar. I p...
New
rms.mrcs
Hi, I need to transform a list of numbers into a map where the keys are the indexes and the values are the original values of the list. ...
New
Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New
shijith.k
I am trying to start a new phoenix project with elixir 1.9, but mix phx.new does not work. It says that ** (Mix) The task "phx.new" could...
New
hariharasudhan94
Lets say I have map like this fetching from my database %{"_id" =&gt; #BSON.ObjectId&lt;58eb1a7a9ad169198c3dXXXX&gt;, "email" =&gt; ...
New

Other popular topics Top

AstonJ
Posting this to see if we can make things easier for people to get into Neovim. If you use Neovim and have a favourite distro please let ...
New
Nvim
Anybody knows a comprehensive comparison of Django and Phoenix, thanks for the help. Where are they similar? Where do they differ the m...
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID&lt;0.412.0&gt; terminating ** (Postgrex.Error) FATAL...
New
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod – where is this set? Thanks.
New
pmjoe
I have a relationship of love and hate with Elixir. Lots of things are just absolutely right, but there are some things that are kind of ...
New
jerry
Good day to you all. I have been struggling to get a query involving like and ilike to work. Can anyone assist me on this, please? pro...
New
vrod
I am using the Starship cross-shell prompt – it seems pretty nice, but I get some errors: [WARN] - (starship::utils): Executing command ...
New
bsollish-terakeet
Credo is smart enough to check for (something like) this: assert length(the_list) == 0 with this response: Checking if an enum is empt...
New
JakeBecker
TL;DR: I’ve just released an implementation of Microsoft’s IDE-independent Language Server Protocol for Elixir. It adds language support ...
1144 54250 245
New
sergio
Kind of like when jquery came out, it was super necessary. Existing drag and drop libraries have a bunch of baggage to support old browse...
New

We're in Beta

About us Mission Statement