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:
- Structure of the error is enforced by compiler
- It is easier to deal with additional error context (Structs have fields)
- Structs can implement protocols
-
Specifically: Structs can implement
String.Charsprotocol - a nice way to get a friendly formatted error message - Structs can implement behaviours
-
Specifically: Structs can implement
Exceptionbehaviour. Client code can just raise error Struct as exception if it deems it sensible
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