Exceptional: Helpers for Elixir exceptions
Installation
If available in Hex, the package can be installed as:
Add
exceptional
to your list of dependencies inmix.exs
:def deps do [{:exceptional, "~> 0.1.0"}] end ```
Ensure
exceptional
is started before your application:def application do [applications: [:exceptional]] end ```
Prior Art
Tagged Status
The tagged status pattern ({:ok, _}
, {:error, _}
, etc)has been the
bread and butter of Erlang since the beginning. While this makes it very easy to
track the meaning of an expression, two things can happen:
- The tag becomes out of sync
- ex.
{:ok, "and yet not ok"}
- Pattern matching becomes challenging when different lengths exist
- ex.
{:error, "oopsie"}
,{:error, "oopsie", %{original: :data, for: "handling"}}
Optimistic Flow
The other alternative is to be optimistic returns, generally seen with bang patterns.
Ex. doc = File.read! path
instead of {:ok, doc} = File.read path"
. This is
more convenient, but will raise
, robbing the caller of control without try/catch
.
Error Monad
Currently a very undersused pattern in the Erlang/Elixir ecosystem, this is probably
“the right way” to do general error handling (or at last the most theoretically pure one).
Essentially, wrap your computation in an ADT struct,
paired with a binding function
(super-powered |>
), that escapes the pipe flow if it encounters an Exception
.
The downside is of course that people are generally afraid of introducing monads into their Elixir code, as understanding it requires some theoretical understanding.
Exceptional
Exceptional
takes a hybrid approach. The aim is to behave similar to an error monad,
but in a more Elixir-y way. This is less powerful than the monad solution, but simpler to
understand fully, and cleaner than optimistic flow, and arguably more convenient than the
classic tagged status.
Examples
Exception or Value
1 ~> fn value -> value * 100 end.()
#=> 100
Enum.OutOfBoundsError.exception("exception") ~> fn x -> x + 1 end.()
#=> %Enum.OutOfBoundsError{message: "exception"}
[1,2,3]
|> hypothetical_returns_exception
~> fn would_be_list ->
would_be_list
|> Enum.map(fn x -> x + 1 end)
end.()
#=> %Enum.OutOfBoundsError{message: "exception"}
0..10
|> Enum.take(3)
~> fn list ->
list
|> Enum.map(fn x -> x + 1 end)
end.()
#=> [1,2,3]
Tagged Status
[1,2,3]
|> hypothetical_returns_exception
~> fn would_be_list ->
would_be_list
|> Enum.map(fn x -> x + 1 end)
end.()
#=> {:error, "exception"}
0..10
|> Enum.take(3)
~> fn list ->
list
|> Enum.map(fn x -> x + 1 end)
end.()
|> to_tagged_status
#=> {:ok, [1,2,3]}
Finally Raise
1 >>> fn x -> x + 1 end.()
#=> 2
%ArgumentError{message: "raise me"} >>> fn x -> x + 1 end.()
#=> ** (ArgumentError) raise me
Write Your Own with Control
Exceptional.Control.branch 1,
value_do: fn v -> v + 1 end.(),
exception_do: fn ex -> ex end.()
#=> 2
ArgumentError.exception("error message"),
|> Exceptional.Control.branch(
value_do: fn v -> v end.(),
exception_do: fn %{message: msg} -> msg end.()
)
#=> "error message"