ExcError

A common way to represent errors in elixir is to return tuples of format {:error, term}. However term is often a simple atom (or another tuple with simple atom). I believe that a better way to represent errors is to use elixir Structs.

They bring some advantages:

This library is a thin wrapper to reduce boilerplate for defining Error structs that implement String.Chars protocol and Exception behaviour.

Feel free to use this “Structs as Errors” pattern but please do not use this Library. You do not need any libs to use some architectural pattern. I have made this library to get rid of annoying duplication in internal company projects. I recommend you to tinker your own version tailored to your specific needs.

However some usage examples:

# Basic Usage

module SomeModule do
  require ExcError

  ExcError.define(SomeError)

  def some_method
    {:error, %SomeError{}}
  end
end

{:error, %SomeError{} = my_error} = SomeModule.some_method()

# You can format error as string
to_string(my_error)

some_text = "error #{my_error}"

# You can raise error as exception
raise my_error

# default type for struct is declared for you
@spec some_function() :: :ok | {:error, SomeError.t()}

# You can define some custom fields for your struct (just like in defstruct)

ExcError.define SomeError, :some_field, other_field: "default_value"

def my_method
  {:error, %SomeError{some_field: "some-field-value"}}
end

# You can define methods for your struct

ExcError.define SomeError do
  def some_method do
    :ok
  end
end

:ok = SomeError.some_method()

# You can define custom implementation for `String.Chars` protocol

ExcError.define HttpError, [:method, :url, :code] do
  @impl true
  def message(exc), do: "HTTP error method:#{exc.method} url:#{exc.url} code:#{exc.code}"
end

# If no custom fields are provided for your struct, ExcError defines :message field by default:

ExcError.define SomeError

"some message" = to_string(%SomeError{message: "some message"})

# You can define custom type for your struct

ExcError.define SomeError, [:custom_field] do
    @type t :: %__MODULE__{
            custom_field: atom
          }
end

# All defined structs have :cause field by default
# Use it to wrap other errors

ExcError.define SomeError

error_struct = SomeError.wrap({:error, :other_error})
:other_error = error_struct.cause

error_struct = SomeError.wrap(%SomeStruct{})
%SomeStruct{} = error_struct.cause

ExcError.define SomeOtherError, [:some_field]

error_struct = SomeOtherError.wrap({:error, :other_error}, some_field: "some-value")
:other_error = error_struct.cause
"some-value" = error_struct.some_field