Error model
loco-rs uses a single crate-wide error type. This page documents its shape, how it becomes an HTTP response, and the helpers for constructing it.
Result and Error
pub type Result<T, E = Error> = std::result::Result<T, E>(src/lib.rs:52).pub use self::errors::Error(src/lib.rs:5) — re-exported at the crate root and inloco_rs::prelude.Erroris declared#[non_exhaustive](src/errors.rs:31). Anymatch Error { .. }outside the crate must include a wildcard_ =>arm — the compiler enforces this, and the crate itself relies on it (see the status map below).
Variant → HTTP status map
impl IntoResponse for Error (src/controller/mod.rs:180-253) matches on the error and produces (StatusCode, ErrorDetail), then serializes ErrorDetail as the JSON response body. Only seven variants are matched explicitly; every other variant falls through to the trailing _ => arm.
| Variant | Produced when | HTTP status | Response ErrorDetail |
|---|---|---|---|
NotFound | Returned by the not_found() helper, or directly. | 404 | {"error":"not_found","description":"Resource was not found"} |
Unauthorized(String) | Returned by the unauthorized(msg) helper, or directly. Also logs tracing::warn!(err) with the original message (the message itself is not sent to the client). | 401 | {"error":"unauthorized","description":"You do not have permission to access this resource"} |
CustomError(StatusCode, ErrorDetail) | Constructed directly when the caller wants an arbitrary status and body. | passthrough — whatever StatusCode was supplied | passthrough — whatever ErrorDetail was supplied |
WithBacktrace { inner, backtrace } | Wraps another error variant; produced by calling .bt() on an Error (backtrace is only captured when RUST_BACKTRACE is set — see Constructors below). Also prints the inner error (red, underlined) and the filtered backtrace to stdout via backtrace::print_backtrace. | 400 | {"error":"Bad Request"} (only the error reason is set, no description) |
BadRequest(String) | Returned by the bad_request(msg) helper, or directly. | 400 | {"error":"Bad Request","description":"<msg>"} |
JsonRejection(JsonRejection) | Axum’s Json extractor rejects a malformed/missing request body (surfaced via the Json<T> wrapper’s #[from_request(rejection(Error))]). Logs tracing::debug!(err = err.body_text(), ...). | err.status() — axum’s own rejection status (commonly 400/415/422) | {"error":"Bad Request"} |
Validation(ModelValidationErrors) | A validator-crate validation failure converted #[from] ModelValidationErrors. | 400 | {"errors": <serde_json::Value of the field errors>} — note error/description are None here; only errors is populated |
everything else (all other variants, current and future — this is what the #[non_exhaustive] catch-all covers) | Any of the ~28 remaining variants (DB, Model, IO, Redis, Sqlx, Tera, YAML, Message, Any, InternalServerError, etc. — see the full list below) | 500 | {"error":"internal_server_error","description":"Internal Server Error"} |
Every response, regardless of variant, is first logged at tracing::error! with error.msg / error.details fields before the match runs (src/controller/mod.rs:184-202).
ErrorDetail — the response body shape
#[derive(Debug, Serialize)]pub struct ErrorDetail { #[serde(skip_serializing_if = "Option::is_none")] pub error: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] pub description: Option<String>, #[serde(skip_serializing_if = "Option::is_none")] pub errors: Option<serde_json::Value>,}(src/controller/mod.rs:133-142)
Constructors:
| Fn | Signature | Behavior |
|---|---|---|
ErrorDetail::new | new<T1, T2>(error: T1, description: T2) -> Self (:147) | Sets error; sets description to None if the passed description is an empty string, Some(..) otherwise. errors is always None. |
ErrorDetail::with_reason | with_reason<T>(error: T) -> Self (:161) | Sets only error; description/errors are None. |
The body is always wrapped in the crate’s own Json<T> type (src/controller/mod.rs:170-178, a thin axum::Json newtype), not raw axum::Json.
Constructor helpers on Error
src/errors.rs:153-177:
| Fn | Signature | Notes |
|---|---|---|
Error::wrap | wrap(err: impl std::error::Error + Send + Sync + 'static) -> Self (:154) | Boxes any error into Error::Any(Box::new(err)). Does not call .bt() (backtrace capture is commented out). |
Error::msg | msg(err: impl std::error::Error + Send + Sync + 'static) -> Self (:158) | Stringifies the error’s Display into Error::Message(err.to_string()). Also does not capture a backtrace. |
Error::string | string(s: &str) -> Self (:162) | Builds Error::Message(s.to_string()) directly from a string slice. #[must_use]. |
Error::bt | bt(self) -> Self (:166) | Captures std::backtrace::Backtrace::capture(). If the backtrace status is Disabled or Unsupported (i.e. RUST_BACKTRACE is unset), returns self unchanged — no allocation, no wrapping. Otherwise wraps self in Error::WithBacktrace. #[must_use]. |
Both wrap/msg are cheap-conversion helpers for turning a foreign std::error::Error into the crate’s Error at a call site (e.g. inside a handler using .map_err(Error::wrap)); bt is the opt-in backtrace wrapper used internally (e.g. the hand-written From<serde_json::Error> impl at src/errors.rs:24-28 does Self::JSON(val).bt()).
Controller helper functions
Free functions in src/controller/mod.rs for the common HTTP-facing variants, each returning Result<U> (i.e. always Err(..)):
| Fn | Signature | file:line |
|---|---|---|
unauthorized | unauthorized<T: Into<String>, U>(msg: T) -> Result<U> | :112 |
bad_request | bad_request<T: Into<String>, U>(msg: T) -> Result<U> | :121 |
not_found | not_found<T>() -> Result<T> | :130 |
All three are re-exported from loco_rs::prelude.
Full variant list
The complete #[non_exhaustive] enum Error (src/errors.rs:32-151), with feature gates where present:
| Variant | Feature gate |
|---|---|
WithBacktrace { inner: Box<Self>, backtrace: Box<Backtrace> } | — |
Message(String) | — |
QueueProviderMissing | — |
TaskNotFound(String) | — |
Scheduler(#[from] crate::scheduler::Error) | — |
Axum(#[from] axum::http::Error) | — |
Tera(#[from] tera::Error) | — |
JSON(serde_json::Error) | — (hand-rolled From, not #[from], so it can call .bt()) |
JsonRejection(#[from] JsonRejection) | — |
YAMLFile(#[source] serde_yaml::Error, String) | — |
YAML(#[from] serde_yaml::Error) | — |
EmailSender(#[from] lettre::error::Error) | — |
Smtp(#[from] smtp::Error) | — |
Worker(String) | — |
IO(#[from] std::io::Error) | — |
DB(#[from] sea_orm::DbErr) | with-db |
ParseAddress(#[from] AddressError) | — |
Unauthorized(String) | — |
NotFound | — |
BadRequest(String) | — |
CustomError(StatusCode, ErrorDetail) | — |
InternalServerError | — |
InvalidHeaderValue(#[from] InvalidHeaderValue) | — |
InvalidHeaderName(#[from] InvalidHeaderName) | — |
InvalidMethod(#[from] InvalidMethod) | — |
Model(#[from] crate::model::ModelError) | with-db |
Redis(#[from] redis::RedisError) | worker_redis |
Sqlx(#[from] sqlx::Error) | worker |
Storage(#[from] crate::storage::StorageError) | — |
Cache(#[from] crate::cache::CacheError) | — |
Generators(#[from] loco_gen::Error) | debug_assertions |
VersionCheck(#[from] depcheck::VersionCheckError) | — |
Any(#[from] Box<dyn std::error::Error + Send + Sync>) | — |
Validation(#[from] ModelValidationErrors) | — |
AxumFormRejection(#[from] axum::extract::rejection::FormRejection) | — |
Removed variants (breaking as of the 1.0 error-enum narrowing)
Commit 4a4a84ee (“narrow the Error enum — drop 4 low-value/leaky variants”) removed four variants that are confirmed absent from current source (src/errors.rs):
EnvVar(#[from] std::env::VarError)Hash(String)SemVer(#[from] semver::Error)TaskJoinError(#[from] tokio::task::JoinError)
Any code that constructs or matches these four no longer compiles. Combined with #[non_exhaustive], every downstream match Error { .. } must carry a _ => arm — this was already required before the removal, but the removal is a reminder that new/removed variants must not break exhaustive matches, and user code must not attempt to rely on exhaustiveness.