Skip to main content
Production error handling has two jobs:
  1. Keep enough typed context inside the application to make correct decisions.
  2. Return stable, safe transport errors to clients without leaking adapter or database internals.
ddd_cqrs_es gives you typed library errors at the repository and event-store layers. Your application should add one application error boundary on top of those library errors, then map that boundary into HTTP, gRPC, server functions, logs, and metrics.

Layered Error Model

Keep each layer responsible for its own errors: Do not convert repository or store errors to String in the shared application service. That erases the distinction between validation, concurrency, unavailable storage, serialization, and internal backend failures.

Library Errors

Repository command paths return RepositoryError:
The standard EventStoreError preserves broad infrastructure categories:
Use these variants directly when deciding status codes. Avoid parsing display strings such as "connection error: ..." or "event store backend error: ...".

Application Boundary Error

Apps should define a single error type close to the application service. The counter app uses CounterAppError for all Leptos server-function, REST, and Spin gRPC command paths.
The important conversion is from RepositoryError into the app boundary error:
That preserves classification while still letting the application choose public messages, log levels, and transport status codes in one place.

Shared Command Service

All command transports should call the same application service. The counter app command service uses AsyncRepository::execute_returning_state, retries expected write conflicts, and returns CounterAppResult<CounterViewDto>.
If a command has already committed and a non-critical notification or projection catch-up fails afterward, do not report the command as failed unless your product requires synchronous projection consistency. Log the failure with enough context and let clients recover from durable event replay.

Transport Mapping

Use one mapping table for every public transport: Public messages should be stable and safe. Internal adapter messages, database URLs, SQL fragments, Redis command details, and serialized payloads belong in logs, not in client responses.

REST JSON Errors

REST endpoints should return structured JSON with a stable code:
The counter app exposes explicit JSON REST routes:
Validation proof:
Expected status is 400, and the body contains error.code = "validation".

Leptos Server Functions

Leptos server functions are framework-owned endpoints. Keep them thin:
Server functions cannot carry the same protocol status shape as REST or gRPC. Log the typed error before converting it to ServerFnError, and return only the public message to the browser.

Spin gRPC

Spin gRPC is served through the HTTP trigger. There is no separate gRPC trigger. Enable it with transport=grpc or transport=both:
Then call the service with grpcurl from examples/counter-app:
Validation proof:
Expected result is a gRPC InvalidArgument error with the public message amount must be positive. Wasmtime currently supports the HTTP transport only. make wasmtime ... transport=grpc and make wasmtime ... transport=both fail fast with a Spin-only transport message.

Tracing and Logs

Initialize tracing_subscriber once at the runtime entrypoint, then use structured logs at the application and transport boundaries:
Do not hold a tracing::Span::entered() guard across .await in Leptos server-function futures. The guard is not Send, and server functions require Send futures. Prefer structured events, or use instrumentation patterns that do not keep an entered guard alive across .await. The counter app Makefile forwards RUST_LOG into Spin and Wasmtime when it is set:
Use this when proving REST, gRPC, SSE, and Redis-trigger behavior from local terminals.

Tests and Verification

Error handling tests should exercise real error values, not fake service stubs:
  • Construct RepositoryError::Domain, RepositoryError::Concurrency, and RepositoryError::Store(EventStoreError::Connection) values.
  • Assert REST status and JSON code/message mapping.
  • Assert gRPC tonic::Code mapping.
  • Assert server-function conversion uses the public message.
  • Compile the runtime combinations that own the transport surface.
Counter app checks:
Docs checks: