Logo

0x3d.Site

is designed for aggregating information.
Welcome
check repository here

Exceptional: Helpers for Elixir exceptions

Build Status Inline docs hex.pm version API Docs license

Table of Contents

Installation

Add exceptional to your list of dependencies in mix.exs:

def deps do
  [{:exceptional, "~> 2.1"}]
end

About

Exceptional is an Elixir library providing helpers for working with exceptions. It aims to make working with plain old (unwrapped) Elixir values more convenient, and to give full control back to calling functions.

See the Medium article for more

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:

  1. The tag becomes out of sync
  • ex. {:ok, "and yet not ok"}
  1. 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.

This is a classic inversion of control, and allows for very flexible patterns. For example, using >>> (ie: raise if exception, otherwise continue) sidesteps the need for separate bang functions.

Just like the classic FP wisdom: if it doubt, pass it back to the caller to handle.

Examples

Make Safe

A simple way to declaw a function that normally raises. (Does not change the behaviour of functions that don't raise).

toothless_fetch = safe(&Enum.fetch!/2)
[1,2,3] |> toothless_fetch.(1)
#=> 2

toothless = safe(&Enum.fetch!/2)
[1,2,3] |> toothless.(999)
#=> %Enum.OutOfBoundsError{message: "out of bounds error"}

safe(&Enum.fetch!/2).([1,2,3], 999)
#=> %Enum.OutOfBoundsError{message: "out of bounds error"}

Escape Hatch

[1,2,3] ~> Enum.sum()
#=> 6

Enum.OutOfBoundsError.exception("exception") ~> Enum.sum
#=> %Enum.OutOfBoundsError{message: "exception"}

[1,2,3]
|> hypothetical_returns_exception()
~> Enum.map(fn x -> x + 1 end)
~> Enum.sum()
#=> %Enum.OutOfBoundsError{message: "exception"}

0..10
|> Enum.take(3)
~> Enum.map(fn x -> x + 1 end)
~> Enum.sum()
#=> 6

Normalize Errors

Elixir and Erlang interoperate, but represent errors differently. normalize normalizes values into exceptions or plain values (no {:error, _} tuples). This can be seen as the opposite of the functions that convert back to tagged status. Some error types may not be detected; but you may pass a custom converter (see examples below).

normalize(42)
#=> 42

normalize(%Enum.OutOfBoundsError{message: "out of bounds error"})
#=> %Enum.OutOfBoundsError{message: "out of bounds error"}

normalize(:error)
#=> %ErlangError{original: nil}

normalize({:error, "boom"})
#=> %ErlangError{original: "boom"}

normalize({:error, {1, 2, 3}})
#=> %ErlangError{original: {1, 2, 3}}

normalize({:error, "boom with stacktrace", ["trace"]})
#=> %ErlangError{original: "boom with stacktrace"}

normalize({:good, "tuple", ["value"]})
#=> {:good, "tuple", ["value"]}

{:oh_no, {"something bad happened", %{bad: :thing}}}
|> normalize(fn
  {:oh_no, {message, _}} -> %File.Error{reason: message} # This case
  {:bang, message}       -> %File.CopyError{reason: message}
  otherwise              -> otherwise
end)
#=> %File.Error{message: msg}

{:oh_yes, {1, 2, 3}}
|> normalize(fn
  {:oh_no, {message, _}} -> %File.Error{reason: message}
  {:bang, message}       -> %File.CopyError{reason: message}
  otherwise              -> otherwise # This case
end)
#=> {:oh_yes, {1, 2, 3}}

Back to Tagged Status

[1,2,3]
|> hypothetical_returns_exception()
~> Enum.map(fn x -> x + 1 end)
~> Enum.sum()
#=>  {:error, "exception"}

0..10
|> Enum.take(3)
~> Enum.map(fn x -> x + 1 end)
~> Enum.sum()
|> to_tagged_status()
#=> {:ok, 6}


0..10
|> hypothetical_returns_exception()
~> Enum.map(fn x -> x + 1 end)
~> Enum.sum()
|> ok()
#=>  {:error, "exception"}


maybe_sum =
  0..10
  |> hypothetical_returns_exception()
  ~> Enum.map(fn x -> x + 1 end)
  ~> Enum.sum()

~~~maybe_sum
#=>  {:error, "exception"}

Finally Raise

Note that this does away with the need for separate foo and foo! functions, thanks to the inversion of control.

[1,2,3] >>> Enum.sum()
#=> 6

%ArgumentError{message: "raise me"} >>> Enum.sum()
#=> ** (ArgumentError) raise me

ensure!([1, 2, 3])
#=> [1, 2, 3]

ensure!(%ArgumentError{message: "raise me"})
#=> ** (ArgumentError) raise me

defmodule Foo do
  use Exceptional

  def! foo(a), do: a
end

Foo.foo([1, 2, 3])
#=> [1, 2, 3]

Foo.foo(%ArgumentError{message: "raise me"})
#=> %ArgumentError{message: "raise me"}

Foo.foo!([1, 2, 3])
#=> [1, 2, 3]

Foo.foo!(%ArgumentError{message: "raise me"})
#=> ** (ArgumentError) raise me

Manually Branch

Exceptional.Control.branch 1,
  value_do: fn v -> v + 1 end.(),
  exception_do: fn %{message: msg} -> msg end.()
#=> 2

ArgumentError.exception("error message"),
|> Exceptional.Control.branch(value_do: fn v -> v end.(), exception_do: fn %{message: msg} -> msg end.())
#=> "error message"

if_exception 1, do: fn %{message: msg} -> msg end.(), else: fn v -> v + 1 end.(),
#=> 2

ArgumentError.exception("error message")
|> if_exception do
  fn %{message: msg} -> msg end.())
else
  fn v -> v end.()
end
#=> "error message"

Related Packages

Elixir
Elixir
Elixir is a dynamic, functional programming language designed for building scalable and maintainable applications. Built on the Erlang VM, it's known for its high concurrency and fault tolerance, making it ideal for real-time systems and web services.
GitHub - chrismccord/atlas: Object Relational Mapper for Elixir
GitHub - chrismccord/atlas: Object Relational Mapper for Elixir
GitHub - mbuhot/ecto_job: Transactional job queue with Ecto, PostgreSQL and GenStage
GitHub - mbuhot/ecto_job: Transactional job queue with Ecto, PostgreSQL and GenStage
GitHub - zamith/tomlex: A TOML parser for elixir
GitHub - zamith/tomlex: A TOML parser for elixir
GitHub - pablomartinezalvarez/glayu: A static site generator for mid-sized sites.
GitHub - pablomartinezalvarez/glayu: A static site generator for mid-sized sites.
GitHub - jui/mustachex: Mustache for Elixir
GitHub - jui/mustachex: Mustache for Elixir
GitHub - joaothallis/elixir-auto-test: Run test when file is saved
GitHub - joaothallis/elixir-auto-test: Run test when file is saved
GitHub - campezzi/ignorant: Simplify comparison of Elixir data structures by ensuring fields are present but ignoring their values.
GitHub - campezzi/ignorant: Simplify comparison of Elixir data structures by ensuring fields are present but ignoring their values.
GitHub - Driftrock/mockingbird: A set of helpers to create http-aware modules that are easy to test.
GitHub - Driftrock/mockingbird: A set of helpers to create http-aware modules that are easy to test.
GitHub - gutschilla/elixir-pdf-generator: Create PDFs with wkhtmltopdf or puppeteer/chromium from Elixir.
GitHub - gutschilla/elixir-pdf-generator: Create PDFs with wkhtmltopdf or puppeteer/chromium from Elixir.
GitHub - antirez/disque: Disque is a distributed message broker
GitHub - antirez/disque: Disque is a distributed message broker
GitHub - jcomellas/ex_hl7: HL7 Parser for Elixir
GitHub - jcomellas/ex_hl7: HL7 Parser for Elixir
GitHub - Cirru/parser.ex: Cirru Parser in Elixir
GitHub - Cirru/parser.ex: Cirru Parser in Elixir
GitHub - thiamsantos/pwned: Check if your password has been pwned
GitHub - thiamsantos/pwned: Check if your password has been pwned
GitHub - suvash/hulaaki: DEPRECATED : An Elixir library (driver) for clients communicating with MQTT brokers(via the MQTT 3.1.1 protocol).
GitHub - suvash/hulaaki: DEPRECATED : An Elixir library (driver) for clients communicating with MQTT brokers(via the MQTT 3.1.1 protocol).
GitHub - sinetris/factory_girl_elixir: Minimal implementation of Ruby's factory_girl in Elixir.
GitHub - sinetris/factory_girl_elixir: Minimal implementation of Ruby's factory_girl in Elixir.
GitHub - navinpeiris/ex_unit_notifier: Desktop notifications for ExUnit
GitHub - navinpeiris/ex_unit_notifier: Desktop notifications for ExUnit
GitHub - DefactoSoftware/test_selector: Elixir library to help selecting the right elements in your tests.
GitHub - DefactoSoftware/test_selector: Elixir library to help selecting the right elements in your tests.
GitHub - xerions/ecto_migrate: Automatic migrations for ecto
GitHub - xerions/ecto_migrate: Automatic migrations for ecto
GitHub - meh/reagent: You need more reagents to conjure this server.
GitHub - meh/reagent: You need more reagents to conjure this server.
GitHub - stevegraham/hypermock: HTTP request stubbing and expectation Elixir library
GitHub - stevegraham/hypermock: HTTP request stubbing and expectation Elixir library
GitHub - msharp/elixir-statistics: Statistical functions and distributions for Elixir
GitHub - msharp/elixir-statistics: Statistical functions and distributions for Elixir
GitHub - Joe-noh/colorful: colorful is justice
GitHub - Joe-noh/colorful: colorful is justice
GitHub - ijcd/taggart: HTML as code in Elixir
GitHub - ijcd/taggart: HTML as code in Elixir
Build software better, together
Build software better, together
GitHub - yeshan333/ex_integration_coveralls: A library for run-time system code line-level coverage analysis.
GitHub - yeshan333/ex_integration_coveralls: A library for run-time system code line-level coverage analysis.
GitHub - PSPDFKit-labs/cobertura_cover: Output test coverage information in Cobertura-compatible format
GitHub - PSPDFKit-labs/cobertura_cover: Output test coverage information in Cobertura-compatible format
GitHub - basho/enm: Erlang driver for nanomsg
GitHub - basho/enm: Erlang driver for nanomsg
GitHub - pawurb/ecto_psql_extras: Ecto PostgreSQL database performance insights. Locks, index usage, buffer cache hit ratios, vacuum stats and more.
GitHub - pawurb/ecto_psql_extras: Ecto PostgreSQL database performance insights. Locks, index usage, buffer cache hit ratios, vacuum stats and more.
GitHub - crate/craterl: Client Libraries for Erlang
GitHub - crate/craterl: Client Libraries for Erlang
GitHub - sheharyarn/ecto_rut: Ecto Model shortcuts to make your life easier! :tada:
GitHub - sheharyarn/ecto_rut: Ecto Model shortcuts to make your life easier! :tada:
Elixir
More on Elixir

Programming Tips & Tricks

Code smarter, not harder—insider tips and tricks for developers.

Error Solutions

Turn frustration into progress—fix errors faster than ever.

Shortcuts

The art of speed—shortcuts to supercharge your workflow.
  1. Collections 😎
  2. Frequently Asked Question's 🤯

Tools

available to use.

Made with ❤️

to provide resources in various ares.