Use custom error macros over anyhow/thiserror
Context
The backend needs structured error codes for GraphQL (ADR-0006), error source chains for debugging, and automatic tracing on error creation. Standard Rust error crates don't provide all three.
anyhow erases error types — you get a string, not a machine-readable code the iOS client can switch on. thiserror gives you typed errors but doesn't integrate with async-graphql's ErrorExtensions trait or OpenTelemetry spans. We'd need glue code on every error type regardless.
The LLM agents also kept getting error handling wrong — wrapping infrastructure errors (UUID parsing, sqlx) in domain error codes instead of propagating them with ?. After cleaning this up across 6 domains (commit 1083dec), we encoded the rules into the macro system so they couldn't be violated.
Decision
A define_error_domain! macro generates everything needed for a domain's error handling from a single enum definition: the error code enum (with GraphQL scalar support), an error struct with source chains, automatic tracing::error! on construction, ErrorExtensions for GraphQL, From impls, and a Result type alias.
Two categories of errors:
Domain errors use the macro — validation failures, authorization errors, business rule violations. These get structured codes that iOS can handle.
Infrastructure errors (sqlx, S3, HTTP clients) propagate with ? via From<Display> impls in async-graphql, producing generic internal errors. They're not wrapped in domain codes.
with_source() preserves the error chain so the underlying cause is always available for debugging, independent of logging configuration.
Consequences
One macro invocation per domain generates all the boilerplate. Error handling rules are enforced by the type system — you can't accidentally wrap an infrastructure error in a domain code because the macro doesn't generate From impls for external error types.
The cost is a complex macro (~175 lines) that new contributors need to understand, and the learning curve of knowing when to use structured codes vs ?.