{"id":13507855,"url":"https://github.com/expede/exceptional","last_synced_at":"2025-05-16T17:07:28.873Z","repository":{"id":54413390,"uuid":"66762717","full_name":"expede/exceptional","owner":"expede","description":"Helpers for Elixir exceptions","archived":false,"fork":false,"pushed_at":"2023-07-28T11:56:15.000Z","size":326,"stargazers_count":294,"open_issues_count":9,"forks_count":10,"subscribers_count":5,"default_branch":"master","last_synced_at":"2025-04-12T15:58:48.519Z","etag":null,"topics":["convenient","elixir","elixir-exceptions","exception-flow","exception-handler","exceptions","macros"],"latest_commit_sha":null,"homepage":"https://hex.pm/packages/exceptional","language":"Elixir","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/expede.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2016-08-28T10:38:08.000Z","updated_at":"2024-11-05T13:13:39.000Z","dependencies_parsed_at":"2024-01-30T08:02:07.122Z","dependency_job_id":"2ac12a56-2c95-41cb-8984-e849e10a2af5","html_url":"https://github.com/expede/exceptional","commit_stats":{"total_commits":60,"total_committers":6,"mean_commits":10.0,"dds":0.4,"last_synced_commit":"2ae2c08023382c6bbe7b135e9adec1e63a0a1fc3"},"previous_names":[],"tags_count":9,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/expede%2Fexceptional","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/expede%2Fexceptional/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/expede%2Fexceptional/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/expede%2Fexceptional/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/expede","download_url":"https://codeload.github.com/expede/exceptional/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254573588,"owners_count":22093731,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["convenient","elixir","elixir-exceptions","exception-flow","exception-handler","exceptions","macros"],"created_at":"2024-08-01T02:00:41.080Z","updated_at":"2025-05-16T17:07:28.854Z","avatar_url":"https://github.com/expede.png","language":"Elixir","funding_links":["https://opencollective.com/exceptional/contribute/tier/8074-backer","https://opencollective.com/exceptional/contribute/tier/8075-sponsor"],"categories":["Errors and Exception Handling","Macros"],"sub_categories":[],"readme":"# Exceptional: Helpers for Elixir exceptions\n![](https://github.com/expede/exceptional/raw/master/branding/logo_with_text.png)\n\n[![Build Status](https://travis-ci.org/expede/exceptional.svg?branch=master)](https://travis-ci.org/expede/exceptional)\n[![Inline docs](http://inch-ci.org/github/expede/exceptional.svg?branch=master)](http://inch-ci.org/github/expede/exceptional)\n[![hex.pm version](https://img.shields.io/hexpm/v/exceptional.svg?style=flat)](https://hex.pm/packages/exceptional)\n[![API Docs](https://img.shields.io/badge/api-docs-MediumPurple.svg?style=flat)](http://hexdocs.pm/exceptional/)\n[![license](https://img.shields.io/github/license/mashape/apistatus.svg?maxAge=2592000)](https://github.com/expede/exceptional/blob/master/LICENSE)\n\n[![](https://opencollective.com/exceptional/tiers/backer.svg?avatarHeight=50)](https://opencollective.com/exceptional/contribute/tier/8074-backer)\n\n[![](https://opencollective.com/exceptional/tiers/sponsor.svg?avatarHeight=50)](https://opencollective.com/exceptional/contribute/tier/8075-sponsor)\n\n## Table of Contents\n- [Installation](#installation)\n- [About](#about)\n- [Prior Art](#prior-art)\n  - [Tagged Status](#tagged-status)\n  - [Optimistic Flow](#optimistic-flow)\n- [Exceptional](#exceptional)\n  - [Examples](#examples)\n    - [Make Safe](#make-safe)\n    - [Escape Hatch](#escape-hatch)\n    - [Normalize Errors](#normalize-errors)\n    - [Back to Tagged Status](#back-to-tagged-status)\n    - [Finally Raise](#finally-raise)\n    - [Manually Branch](#manually-branch)\n- [Related Packages](#related-packages)\n\n## Installation\n\nAdd `exceptional` to your list of dependencies in `mix.exs`:\n```elixir\ndef deps do\n  [{:exceptional, \"~\u003e 2.1\"}]\nend\n```\n\n## About\nExceptional is an Elixir library providing helpers for working with exceptions.\nIt aims to make working with plain old (unwrapped) Elixir values more convenient,\nand to give full control back to calling functions.\n\nSee the [Medium article](https://medium.com/the-monad-nomad/exceptional-freedom-from-error-s-eaabfae25d72#.zgbne4gja) for more\n\n## Prior Art\n### Tagged Status\nThe tagged status pattern (`{:ok, _}`, `{:error, _}`, etc)has been the\nbread and butter of Erlang since the beginning. While this makes it very easy to\ntrack the meaning of an expression, two things can happen:\n\n1. The tag becomes out of sync\n  - ex. `{:ok, \"and yet not ok\"}`\n\n2. Pattern matching becomes challenging when different lengths exist\n  - ex. `{:error, \"oopsie\"}`, `{:error, \"oopsie\", %{original: :data, for: \"handling\"}}`\n\n### Optimistic Flow\nThe other alternative is to be optimistic returns, generally seen with bang patterns.\nEx. `doc = File.read! path` instead of `{:ok, doc} = File.read path\"`. This is\nmore convenient, but will `raise`, robbing the caller of control without `try/catch`.\n\n### Error Monad\nCurrently a very undersused pattern in the Erlang/Elixir ecosystem, this is probably\n\"the right way\" to do general error handling (or at last the most theoretically pure one).\nEssentially, wrap your computation in an [ADT struct](https://hex.pm/packages/algae),\npaired with a [binding function](https://hexdocs.pm/witchcraft/Witchcraft.Monad.Operator.html#%3E%3E%3E/2)\n(super-powered `|\u003e`), that escapes the pipe flow if it encounters an `Exception`.\n\nThe downside is of course that people are generally afraid of introducing monads into\ntheir Elixir code, as understanding it requires some theoretical understanding.\n\n## Exceptional\n`Exceptional` takes a hybrid approach. The aim is to behave similar to an error monad,\nbut in a more Elixir-y way. This is less powerful than the monad solution, but simpler to\nunderstand fully, and cleaner than optimistic flow, and arguably more convenient than the\nclassic tagged status.\n\nThis is a classic inversion of control, and allows for very flexible patterns.\nFor example, using [`\u003e\u003e\u003e`](#finally-raise) (ie: `raise` if exception, otherwise continue) sidesteps\nthe need for separate bang functions.\n\nJust like the classic FP wisdom: if it doubt, pass it back to the caller to handle.\n\n## Examples\n\n### [Make Safe](https://hexdocs.pm/exceptional/Exceptional.Safe.html)\n\nA simple way to declaw a function that normally `raise`s. (Does not change the behaviour of functions that don't `raise`).\n\n```elixir\ntoothless_fetch = safe(\u0026Enum.fetch!/2)\n[1,2,3] |\u003e toothless_fetch.(1)\n#=\u003e 2\n\ntoothless = safe(\u0026Enum.fetch!/2)\n[1,2,3] |\u003e toothless.(999)\n#=\u003e %Enum.OutOfBoundsError{message: \"out of bounds error\"}\n\nsafe(\u0026Enum.fetch!/2).([1,2,3], 999)\n#=\u003e %Enum.OutOfBoundsError{message: \"out of bounds error\"}\n```\n\n### [Escape Hatch](https://hexdocs.pm/exceptional/Exceptional.Value.html)\n\n```elixir\n[1,2,3] ~\u003e Enum.sum()\n#=\u003e 6\n\nEnum.OutOfBoundsError.exception(\"exception\") ~\u003e Enum.sum\n#=\u003e %Enum.OutOfBoundsError{message: \"exception\"}\n\n[1,2,3]\n|\u003e hypothetical_returns_exception()\n~\u003e Enum.map(fn x -\u003e x + 1 end)\n~\u003e Enum.sum()\n#=\u003e %Enum.OutOfBoundsError{message: \"exception\"}\n\n0..10\n|\u003e Enum.take(3)\n~\u003e Enum.map(fn x -\u003e x + 1 end)\n~\u003e Enum.sum()\n#=\u003e 6\n```\n\n### [Normalize Errors](https://hexdocs.pm/exceptional/Exceptional.Normalize.html)\n\nElixir and Erlang interoperate, but represent errors differently. `normalize` normalizes values into exceptions or plain values (no `{:error, _}` tuples).\nThis can be seen as the opposite of the functions that convert back to [tagged status](#back-to-tagged-status).\nSome error types may not be detected; but you may pass a custom converter (see examples below).\n\n```elixir\nnormalize(42)\n#=\u003e 42\n\nnormalize(%Enum.OutOfBoundsError{message: \"out of bounds error\"})\n#=\u003e %Enum.OutOfBoundsError{message: \"out of bounds error\"}\n\nnormalize(:error)\n#=\u003e %ErlangError{original: nil}\n\nnormalize({:error, \"boom\"})\n#=\u003e %ErlangError{original: \"boom\"}\n\nnormalize({:error, {1, 2, 3}})\n#=\u003e %ErlangError{original: {1, 2, 3}}\n\nnormalize({:error, \"boom with stacktrace\", [\"trace\"]})\n#=\u003e %ErlangError{original: \"boom with stacktrace\"}\n\nnormalize({:good, \"tuple\", [\"value\"]})\n#=\u003e {:good, \"tuple\", [\"value\"]}\n\n{:oh_no, {\"something bad happened\", %{bad: :thing}}}\n|\u003e normalize(fn\n  {:oh_no, {message, _}} -\u003e %File.Error{reason: message} # This case\n  {:bang, message}       -\u003e %File.CopyError{reason: message}\n  otherwise              -\u003e otherwise\nend)\n#=\u003e %File.Error{message: msg}\n\n{:oh_yes, {1, 2, 3}}\n|\u003e normalize(fn\n  {:oh_no, {message, _}} -\u003e %File.Error{reason: message}\n  {:bang, message}       -\u003e %File.CopyError{reason: message}\n  otherwise              -\u003e otherwise # This case\nend)\n#=\u003e {:oh_yes, {1, 2, 3}}\n```\n\n### [Back to Tagged Status](https://hexdocs.pm/exceptional/Exceptional.TaggedStatus.html)\n\n```elixir\n[1,2,3]\n|\u003e hypothetical_returns_exception()\n~\u003e Enum.map(fn x -\u003e x + 1 end)\n~\u003e Enum.sum()\n#=\u003e  {:error, \"exception\"}\n\n0..10\n|\u003e Enum.take(3)\n~\u003e Enum.map(fn x -\u003e x + 1 end)\n~\u003e Enum.sum()\n|\u003e to_tagged_status()\n#=\u003e {:ok, 6}\n\n\n0..10\n|\u003e hypothetical_returns_exception()\n~\u003e Enum.map(fn x -\u003e x + 1 end)\n~\u003e Enum.sum()\n|\u003e ok()\n#=\u003e  {:error, \"exception\"}\n\n\nmaybe_sum =\n  0..10\n  |\u003e hypothetical_returns_exception()\n  ~\u003e Enum.map(fn x -\u003e x + 1 end)\n  ~\u003e Enum.sum()\n\n~~~maybe_sum\n#=\u003e  {:error, \"exception\"}\n\n```\n\n### [Finally Raise](https://hexdocs.pm/exceptional/Exceptional.Raise.html)\n\nNote that this does away with the need for separate `foo` and `foo!` functions,\nthanks to the inversion of control.\n\n```elixir\n[1,2,3] \u003e\u003e\u003e Enum.sum()\n#=\u003e 6\n\n%ArgumentError{message: \"raise me\"} \u003e\u003e\u003e Enum.sum()\n#=\u003e ** (ArgumentError) raise me\n\nensure!([1, 2, 3])\n#=\u003e [1, 2, 3]\n\nensure!(%ArgumentError{message: \"raise me\"})\n#=\u003e ** (ArgumentError) raise me\n\ndefmodule Foo do\n  use Exceptional\n\n  def! foo(a), do: a\nend\n\nFoo.foo([1, 2, 3])\n#=\u003e [1, 2, 3]\n\nFoo.foo(%ArgumentError{message: \"raise me\"})\n#=\u003e %ArgumentError{message: \"raise me\"}\n\nFoo.foo!([1, 2, 3])\n#=\u003e [1, 2, 3]\n\nFoo.foo!(%ArgumentError{message: \"raise me\"})\n#=\u003e ** (ArgumentError) raise me\n\n```\n\n### [Manually Branch](https://hexdocs.pm/exceptional/Exceptional.Control.html)\n\n```elixir\nExceptional.Control.branch 1,\n  value_do: fn v -\u003e v + 1 end.(),\n  exception_do: fn %{message: msg} -\u003e msg end.()\n#=\u003e 2\n\nArgumentError.exception(\"error message\"),\n|\u003e Exceptional.Control.branch(value_do: fn v -\u003e v end.(), exception_do: fn %{message: msg} -\u003e msg end.())\n#=\u003e \"error message\"\n\nif_exception 1, do: fn %{message: msg} -\u003e msg end.(), else: fn v -\u003e v + 1 end.(),\n#=\u003e 2\n\nArgumentError.exception(\"error message\")\n|\u003e if_exception do\n  fn %{message: msg} -\u003e msg end.())\nelse\n  fn v -\u003e v end.()\nend\n#=\u003e \"error message\"\n```\n\n## Related Packages\n\n- [Phoenix/Exceptional](https://hex.pm/packages/phoenix_exceptional)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fexpede%2Fexceptional","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fexpede%2Fexceptional","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fexpede%2Fexceptional/lists"}