View Source Errors, escaping and halting

As a request is moving through a pipeline, some steps may produce errors. In that case the pipeline stops and returns the first error encountered, without calling the rest of the pipes.

This is not always the desired behavior. Let's consider a pipeline where the first few pipes validate different aspects of the request.

defmodule Example do
  use Plumbery
  import Plumbery.Request

  defp validate_x(req = %{command: %{x: x}}) when is_number(x) and x > 0, do: req
  defp validate_x(req), do: add_error(req, :x, "is invalid")

  defp validate_y(req = %{command: %{y: y}}) when is_number(y) and y > 0, do: req
  defp validate_y(req), do: add_error(req, :y, "is invalid")

  defp validate_name(req = %{command: %{name: "John"}}), do: req
  defp validate_name(req), do: add_error(req, :name, "not John")

  pipeline :validate_number_y do
    pipe :validate_y
  end

  defp calculate(req = %{command: command}) do
    success(req, "Hello #{command.name}! x+y = #{command.x + command.y}")
  end

  pipeline :run do
    pipe :validate_x
    pipe :validate_number_y
    pipe :validate_name
    pipe :calculate
  end
end

# We expect to see 3 errors, but this fails
%{result: {:error, [x: "is invalid", y: "is invalid", name: "not John"]}} =
  Example.run(%Plumbery.Request{command: %{name: "Jim", x: -20, y: 0}})

# This is what we actually get
%{result: {:error, [x: "is invalid"]}} =
  Example.run(%Plumbery.Request{command: %{name: "Jim", x: -20, y: 0}})

When calling the pipeline, we want it to report validation errors for all aspects, not just the one it happens to check first. That is where the concept of escaping comes into play. By default a pipeline escapes on error, meaning that as soon as the request has an error, the pipeline returns immediately without calling any further steps. After escaping is turned off, all subsequent pipes will be called regardless of wheter the request has errors or not. That provides an opportunity for counter-acting an error or adding another error that is orthogonal to the ones generated by previous steps.

Let's modify the example so it actually does what we want.

defmodule Example do
  use Plumbery
  import Plumbery.Request

  defp validate_x(req = %{command: %{x: x}}) when is_number(x) and x > 0, do: req
  defp validate_x(req), do: add_error(req, :x, "is invalid")

  defp validate_y(req = %{command: %{y: y}}) when is_number(y) and y > 0, do: req
  defp validate_y(req), do: add_error(req, :y, "is invalid")

  defp validate_name(req = %{command: %{name: "John"}}), do: req
  defp validate_name(req), do: add_error(req, :name, "not John")

  pipeline :validate_number_y do
    escape_on_error false
    pipe :validate_y
  end

  defp calculate(req = %{command: command}) do
    success(req, "Hello #{command.name}! x+y = #{command.x + command.y}")
  end

  pipeline :run do
    escape_on_error false
    pipe :validate_x
    pipe :validate_number_y
    pipe :validate_name

    escape_on_error true
    pipe :calculate
  end
end

# Now all expected errors are reported
%{result: {:error, [x: "is invalid", y: "is invalid", name: "not John"]}} =
  Example.run(%Plumbery.Request{command: %{name: "Jim", x: -20, y: 0}})

%{result: {:error, [x: "is invalid", y: "is invalid"]}} =
  Example.run(%Plumbery.Request{command: %{name: "John", x: -20, y: 0}})

# And the `:calculate' pipe is called only if there were no errors
%{result: {:ok, "Hello John! x+y = 99"}} =
  Example.run(%Plumbery.Request{command: %{name: "John", x: 90, y: 9}})

So how does escape_on_error options work? That's pretty simple. If it is true (the default value) then as soon as there is an error in the request, the pipeline returns immediately. When that option is false, all subsequent pipes will be called.

Plumbery provides one more way of controlling the pipeline – pipeline halting. Any pipe can call Plumbery.Request.halt/1. That causes the pipeline to return immeditely and unconditionally.

defmodule Example do
  use Plumbery
  import Plumbery.Request

  def halter(req) do
    req
    |> success(:halted)
    |> halt()
  end

  def skipper(req) do
    req
    |> success(:skipped)
  end

  pipeline :haltable do
    pipe :halter
    pipe :skipper
  end
end

# skipper will not have a chance to be called because halter halts the pipeline
%{result: {:ok, :halted}, halted?: true} =
  Example.haltable(%Plumbery.Request{})