Error Handling
This page describes the general model for handling errors in Sword. The details of each transport live in the practical guides of each one.
Principles
- Errors are typed as enums, not as strings.
- The format returned to clients is uniform within each protocol.
- Only what each variant declares is exposed; internal details are not exposed.
Domain errors, not transport errors
Services, repositories, and the other components of a module should not return HTTP errors or gRPC Status values. Instead, each module defines its own errors as a typed enum. The usual way is to describe each variant with thiserror:
use thiserror::Error;
#[derive(Debug, Error)]
pub enum UserError {
#[error("User not found")]
NotFound,
#[error("User already exists")]
AlreadyExists,
}The natural design of thiserror is agnostic, so Sword provides abstractions to transform the variants into transport errors without coupling the domain to them.
Translation per transport
Each transport provides a macro that turns the domain enum into the response it needs. Attributes on each variant declare how it is translated:
| Transport | Macro | Result | Documentation |
|---|---|---|---|
| Web | HttpError | JsonResponse | See here |
| gRPC | GrpcError | tonic::Status | See here |
In both cases the procedure is the same: you derive the macro on the enum and annotate each variant with the code and the message you want to expose.
Propagation and composition
Inside your functions you can propagate the error with ?. A controller can combine errors from several modules using transparent variants, which delegate the mapping to the inner error, and #[from], which converts between types when chaining them:
#[derive(Debug, Error, HttpError)]
pub enum AppError {
#[error("User error: {0}")]
#[http(transparent)]
User(#[from] UserError),
}This way a module's error keeps its mapping even when it crosses another module's boundary.
Tracing
Each variant can declare its own tracing level (trace, debug, info, warn, or error). If you don't set one, the level is derived from the transport's response code. That keeps the logs aligned with the real severity of the error without repeating it in every handler.

