Current section
Files
Jump to
Current section
Files
lib/flux_error.ex
defmodule FluxError do
@moduledoc """
Exception module to improve error management.
"""
@moduledoc since: "0.0.1"
require Logger
@typedoc """
`FluxError` exception struct.
The `:reason` will be used to identify the error message.
The `:metadata` will be used to inject data into the error message.
"""
@typedoc since: "0.0.1"
@type t :: %FluxError{
reason: atom,
metadata: map
}
defexception [:reason, :metadata]
@default_options Application.compile_env(:flux_error, :default_options, [])
@messages_module Application.compile_env(
:flux_error,
:messages_module,
FluxError.DefaultMessages
)
@doc """
Create `t:FluxError.t/0` from an external error.
## Parameters
- `error` - An `t:atom/0` or `t:Exception.t/0`. If `t:atom/0` which will be
inserted at the metadata `:detail` key. If `t:Exception.t/0`, it will try
to get the `:reason` value, otherwise it will generate a `t:String.t/0`
from the `t:Exception.t/0` map.
- `reason` - An `t:atom/0` identifying the error. Defaults to
`:unhandled_error`.
- `metadata` - A `t:keyword/0` with detailed data about the error. Defaults
to empty `t:list/0`.
## Examples
iex> FluxError.from(:external_failure, :external_module_failed, module: :external)
%FluxError{
reason: :external_module_failed,
metadata: %{
detail: :external_failure,
module: :external
}
}
"""
@doc since: "0.0.1"
@spec from(Exception.t() | atom, atom, keyword) :: FluxError.t()
def from(error, reason \\ :unhandled_error, metadata \\ []) do
detail =
if is_atom(error) do
error
else
Map.get(error, :reason, inspect(error))
end
new(reason, [detail: detail] ++ metadata)
end
@doc """
Log the error, returning the `t:FluxError.t/0`.
It will create a `t:FluxError.t/0` if error is an `t:atom/0` or other
`t:Exception.t/0` using `FluxError.from/3`.
The log will have two metadata values:
- `:error_metadata` - The inspection of the `t:FluxError.t/0` `:metadata` key.
- `:error_stacktrace` - The error stacktrace.
## Parameters
- `error` - An `t:atom/0` or `t:Exception.t/0`.
- `stacktrace` - The `t:Exception.stacktrace/0` of the error.
## Examples
iex> try do
...> throw(:failed)
...> catch
...> error -> FluxError.log(error, __STACKTRACE__)
...> end
%FluxError{
reason: :unhandled_error,
metadata: %{
detail: :failed
}
}
"""
@doc since: "0.0.1"
@spec log(Exception.t() | atom, Exception.stacktrace()) :: FluxError.t()
def log(error, stacktrace) do
error = check_error(error)
Logger.error("#{error.reason}: #{message(error)}",
error_metadata: inspect(error.metadata),
error_stacktrace: Exception.format(:error, error, stacktrace)
)
error
end
@doc """
Extract the error message.
It will create a `t:FluxError.t/0` if error is an `t:atom/0` or other
`t:Exception.t/0` using `FluxError.from/3`.
The message is extracted based on `:messages_module` app config. Check
`FluxError` for more information.
## Parameters
- `error` - An `t:atom/0` or `t:Exception.t/0`.
## Examples
iex> FluxError.message(:failed)
"triggered unhandled error"
"""
@doc since: "0.0.1"
@spec message(Exception.t() | atom) :: String.t()
def message(error) do
error = check_error(error)
Enum.reduce(error.metadata, message_raw(error), &fetch_metadata/2)
end
@doc """
Create `t:FluxError.t/0`.
## Parameters
- `reason` - An `t:atom/0` identifying the error.
- `metadata` - A `t:keyword/0` with detailed data about the error. Defaults
to empty `t:list/0`.
## Examples
iex> FluxError.new(:failed, level: :severe)
%FluxError{
reason: :failed,
metadata: %{
level: :severe
}
}
"""
@doc since: "0.0.1"
@spec new(atom, keyword) :: FluxError.t()
def new(reason, metadata \\ []) do
%FluxError{reason: reason, metadata: Map.new(metadata)}
end
@doc """
Generate a `t:map/0` based on error data.
It will create a `t:FluxError.t/0` if error is an `t:atom/0` or other
`t:Exception.t/0` using `FluxError.from/3`.
## Parameters
- `error` - An `t:atom/0` or `t:Exception.t/0`.
- `opts` - A `t:keyword/0` with message options. Check `FluxError.Messages`
for more information. Defaults to empty `t:list/0`
## Examples
iex> FluxError.to_map(:failed)
%{
error: true,
id: :unhandled_error,
message: "triggered unhandled error",
message_raw: "triggered unhandled error",
metadata: %{
detail: :failed
}
}
"""
@doc since: "0.0.1"
@spec to_map(Exception.t() | atom, keyword | nil) :: map
def to_map(error, opts \\ []) do
opts = Keyword.merge(@default_options, opts)
error = check_error(error)
%{
error: true,
id: error.reason,
message: message(error),
message_raw: message_raw(error, opts),
metadata: error.metadata
}
end
defp fetch_metadata({key, value}, message) do
String.replace(message, "%{#{key}}", fn _ -> to_string(value) end)
end
defp message_raw(error, opts \\ []) do
apply(@messages_module, :get_message, [error.reason, opts])
end
defp check_error(%FluxError{} = error), do: error
defp check_error(error), do: from(error)
end