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
chokchit
** (DBConnection.ConnectionError) connection not available and request was dropped from queue after 2733ms. You can configure how long re...
New
Kurisu
For example for a current url like http://localhost:4000/cosmetic/products?_utf8=✓&amp;query=perfume&amp;page=2, I would like to get: ...
New
skosch
To my knowledge, put_in, Map.update etc. all have the one limitation of not automatically creating intermediate keys when needed (for exa...
New
fireproofsocks
Forgive me if this is obvious, but how does one delete a database record WITHOUT selecting it first? Ecto.Repo — Ecto v3.14.0 has exampl...
New
itssasanka
Hi all, Trying to get some more clarity over utc_datetime and naive_datetime for Ecto: The documentation above suggests that while ...
New
Emily
I have VueJS GUIs with the project generated using Webpack. I have Elixir modules that will need to be used by the VueJS GUIs. I forese...
New
sergio_101
I am VERY much an elixir newbie. I have taken one elixir course and one phoenix course on Udemy. During that course, I saw the instructor...
New
nobody
Hi! In PHP: $_SERVER[‘SERVER_ADDR’] - in Elixir? Searched the docs for ip address and the web, no good results. Thanks!
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

Other popular topics Top

hariharasudhan94
lets say i have a sample like a = 20; b = 10; if (a &gt; b) do {:ok, "a"} end if (a &lt; b) do {:ok, b} end if (a == b) do {:ok, "equa...
New
siddhant3030
Hi, I have to write a raw query for one of my project. But till now I have used ecto queries and don’t have much experience writing raw ...
New
marius95
Hello everyone, I try to use an Javascript Event Handler in my root.html.leex file. Therefore I created a function in the app.js file: ...
New
electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
msaraiva
Surface is an experimental library built on top of Phoenix LiveView and its new LiveComponent API that aims to provide a more declarative...
564 43806 214
New
chrismccord
This release brings a number of exciting features, including integration with the new Phoenix LiveDashboard and Phoenix LiveView. There h...
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
klo
Got a question about when to concat vs. prepending items to list then reversing to achieve appending. So i know lists boil down to [1 | ...
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
vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New

We're in Beta

About us Mission Statement