• Stars
    star
    282
  • Rank 145,582 (Top 3 %)
  • Language
    Elixir
  • License
    MIT License
  • Created about 8 years ago
  • Updated about 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Helpers for Elixir exceptions

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

More Repositories

1

awesome-relay

Awesome resources for Relay
269
star
2

awesome-culture

A curated list of awesome thought on tech culture. Inspired by the various awesome-* projects
156
star
3

teaching-fp

Techniques, advice, and anecdotes about how to teach Functional Programming
57
star
4

quick_chat

A quick demo CLI chat app for a workshop
Elixir
23
star
5

rescue

🚒✨ Rescue: better errors through types (a more type directed MonadThrow/MonadCatch)
Haskell
19
star
6

awesome-quantum-computing

A curated list of awesome quantum computing resources. Inspired by the various awesome-* projects
10
star
7

phoenix_exceptional

Exceptional error/exception helpers for Phoenix
Elixir
8
star
8

inspector_gadget

Helpers for debugging & inspecting code flow
Elixir
7
star
9

idris-styleguide

An Idris style guide
4
star
10

settler

Dead simple Scotty server for demo purposes
Haskell
3
star
11

up_run

Course examples for Up & Running with Elixir & Phoenix
Elixir
3
star
12

fainbracket

A simple Brainfuck interpreter in Racket
Racket
3
star
13

expede

2
star
14

exposition

A modern Haskell Prelude focused on consistency and pedagogy
Haskell
2
star
15

config

Setup files
Emacs Lisp
2
star
16

flower

💦 Write even more understandable Haskell
Haskell
2
star
17

validated-token

ERC902 implementation
JavaScript
2
star
18

todo-example

Example Phoenix todo app
Elixir
1
star
19

chipmunk

CHIP-8 emulator
Haskell
1
star
20

starter_kit

Elixir project starter kit
Elixir
1
star
21

TRiiOR

Tail Recursion is its Own Reward — Hakyll Blog
HTML
1
star
22

nesell

A Haskell NES emulator
Haskell
1
star
23

chipper

A CHIP-8 emulator
Nix
1
star
24

haskell-for-typescript-devs

1
star
25

ethmagic.org

A dead simple redirect for Ethereum Magicians
Rust
1
star
26

brainfucker

A simple Brainfuck interpreter
Haskell
1
star
27

gca-rust

WIP WIP WIP 👩‍🔬 Experiment porting generalized cryptographic accumulator to Rust
Rust
1
star