jvoegele

jvoegele

Errata (structured error handling for Elixir) - 1.0.0 released

After a good run of 0.x releases, Errata 1.0.0 is here — the first stable, production-ready release. The public API is now covered by Semantic Versioning, so you can depend on it with confidence.

Errata was first announced here; this thread is the 1.0 milestone, with the full picture of what the library does now.

What is Errata?

Errata is a library for structured, named error handling. In Elixir we usually signal failure either by returning {:error, reason} or by raising an exception — but an ad-hoc reason atom (or worse, a string) carries no context once it’s far from where it was created, and plain exceptions lack a common shape to build logging and reporting around.

Errata replaces both with named error types that share a consistent structure and carry full context about what went wrong and where. The same type works as a raise-able exception and as a value you return in an {:error, _} tuple.

Define an error type in one line

defmodule MyApp.Orders.PaymentDeclined do
  use Errata.DomainError,
    default_message: "the payment was declined",
    reasons: [:insufficient_funds, :fraud_suspected, :card_expired]
end

That generates an exception struct, the Errata.Error behaviour, and String.Chars + Jason.Encoder implementations. Errors come in three kindsdomain, infrastructure, and general — so boundary code can treat business errors differently from system failures.

Every error carries its context

Each Errata error has a well-defined shape:

  • message — a human-readable description
  • reason — an atom that classifies the error (optionally a declared, validated set)
  • context — arbitrary metadata captured at the site of the error
  • cause — a lower-level error this one wrapped, preserving the original
  • env — the module, function, file, line, and stacktrace where it was created

Because all of that travels with the error, you can create it deep in your code and then log, report, or render it to JSON at a boundary without losing the information needed to interpret it. This pays off especially in with expressions: when each error is a structured type that carries its own context, you can drop the else clause and let errors propagate to a boundary where they’re handled — no loss of detail.

At a boundary, it all comes together

# A Phoenix fallback controller that handles *any* Errata error uniformly:
def call(conn, {:error, error}) when Errata.is_error(error) do
  Errata.report(error, log: :warning)   # structured Logger metadata + an [:errata, :error] telemetry event

  conn
  |> put_status(Errata.http_status(error))         # :domain → 422, :infrastructure → 503, :general → 500
  |> json(%{error: Errata.display_message(error)}) # the user-facing message, distinct from the dev message
end

A few of the things that round out 1.0:

  • Rich creationErrata.create/2 builds an error of any type while capturing the call site, and wrap/2 translates a lower-level failure into one of your own error types without losing the original (rescue e -> PaymentGateway.wrap(e, stacktrace: __STACKTRACE__, reason: :timeout)).
  • Context enrichmentput_context/3 and merge_context/2 add to an error’s context as it propagates up a with chain, without rebuilding the struct.
  • Declared reasons — enumerate a type’s valid reasons and have them validated, with a reason/0 type generated into your docs.
  • Error reportingErrata.log/2 attaches the error’s fields as structured Logger metadata; Errata.report/2 emits a [:errata, :error] telemetry event. It’s a vendor-neutral seam: attach a handler that forwards to Sentry, a metrics backend, or wherever — Errata stays out of the integration business.
  • HTTP status mapping — an overridable http_status/1 on every error type, defaulting off its kind.
  • Cause chainingcause/1, root_cause/1, and format_chain/1 for following and rendering a chain of wrapped errors.
  • Classification guardsis_error/1, is_domain_error/1, and is_infrastructure_error/1 for branching at boundaries.

Install

{:errata, "~> 1.0"}

There’s plenty more in the docs — choosing between a distinct error type and a :reason, the domain/infrastructure/general distinction, handling errors with the custom guards, and serialization to JSON.

Thanks to everyone who’s tried Errata and shared feedback along the way; it genuinely shaped the road to 1.0. Feedback and contributions are always welcome.

Links

Most Liked

cmo

cmo

Can you make Jason optional and implement JSON.Encoder now that we have built in JSON support?

jvoegele

jvoegele

Thanks for the suggestion @cmo!

I’ve released 1.1.0 to address this. On Elixir 1.18+ every Errata error type now implements the built-in JSON.Encoder protocol, so JSON.encode!(error) works with no third-party dependencies, and jason becomes an optional dependency. If Jason is present you still get a Jason.Encoder implementation exactly as before, so nothing breaks for existing users — both backends produce the same JSON shape. Projects on 1.18+ that don’t otherwise use Jason can simply drop it.

Where Next?

Popular in News & Updates Top

josevalim
Hi everyone, just a heads up that we announced https://livebook.dev, a website for everyone to learn more about Livebook. One of the coo...
New
jimsynz
As some of you know, I have been working on a tight integration between Ash and Reactor which has triggered a few small bug fixes in Spar...
New
DominikWolek
We have released Membrane Core v0.12.1 :tada::tada::tada: This release will help smooth the transition to the upcoming v1.0.0. See the r...
New
zachdaniel
Hey folks! We’re starting a new weekly newsletter with the goings on of the various Ash packages and other interesting news from myself a...
New
zachdaniel
The new Ash Framework site and installer are live. Please try it out and let me know what you think!
New
zachdaniel
We’re working hard on improving docs, and have some substantial things that it would be great to get some thoughts on. One of the bigges...
New
zachdaniel
Hey folks! I’ll be teasing some interesting bits going into Ash 3.0 while I work on it, and this is post #1! You can follow along with t...
New
bartblast
Hey! For those following Hologram’s progress… I’m excited to share that I’ve just published the official roadmap for Hologram. You can ch...
New
jjcarstens
Hey friends! :waving_hand: With NervesConf 2024 around the corner, Frank, myself, and the greater Nerves team wanted to share a survey w...
New
jjcarstens
NervesPack 0.4.0 is a minor bump with a fairly big change. This update drops :nerves_firmware_ssh in favor of the newer and more focused...
New

Other popular topics Top

danschultzer
None of the current solutions worked well for me, so I went ahead and built a user management system from scratch. This project took far...
548 29377 241
New
Harrisonl
We have an ECS cluster with 4 services, where each task joins a single cluster, via discovery ECS discovery service. Currently when I de...
New
TunkShif
This post is an instruction guide to help you setup your Neovim for Elixir development from scratch. It includes general information on h...
274 41539 114
New
skosch
To my knowledge, put_in, Map.update etc. all have the one limitation of not automatically creating intermediate keys when needed (for exa...
New
chrismccord
As promised, the first release candidate of Phoenix 1.3.0 is out! This release focuses on code generators with improved project structure...
New
chrismccord
This release brings a number of exciting features, including integration with the new Phoenix LiveDashboard and Phoenix LiveView. There h...
New
dblack
I’ve got an issue with an app and I’ve no idea of how to troubleshoot it. I’m hoping someone here might have seen something similar. I p...
New
rms.mrcs
Hi, I need to transform a list of numbers into a map where the keys are the indexes and the values are the original values of the list. ...
New
joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 records...
New
hariharasudhan94
I would like to know what is the best IDE for elixir development?
New

We're in Beta

About us Mission Statement