axum_thiserror #
axum_thiserror is a library that offers a procedural macro to allow thiserror error types to be used as axum responses.
Usage #
Add the library to your current project using Cargo:
cargo add axum_thiserror
Then you can create a basic thiserror error:
#[derive(Error, Debug)]
pub enum UserCreateError {
#[error("User {0} already exists")]
UserAlreadyExists(String),
}
Now you can use axum_thiserror to implement IntoResponse on your error. By
default (the utoipa feature), the response is the variant's status code with
axum::Json(self) as the body, so the enum must also derive serde::Serialize
and utoipa::ToSchema:
[dependencies]
axum_thiserror = "0.3"
serde = { version = "1", features = ["derive"] }
utoipa = "5"
#[derive(Error, Debug, Serialize, ToSchema, ErrorStatus)]
pub enum UserCreateError {
#[error("User {0} already exists")]
#[status(StatusCode::CONFLICT)]
UserAlreadyExists(String),
}
responds with status 409 and body:
{ "UserAlreadyExists": ["user01"] }
The body is whatever serde produces for your enum — shape it with regular serde attributes. A missing implementation is a deliberate compile error rather than a silent fallback to plain text.
Features #
Three cargo features control the response body and the compile-time assertions on the enum:
json— respond withaxum::Json(self); the enum must implementserde::Serialize.utoipa(default, impliesjson) — additionally requiresutoipa::ToSchemaand derivesutoipa::IntoResponsesfrom the same#[status(...)]attributes.schemars(impliesjson) — additionally requiresschemars::JsonSchema, giving a schema matching the response body exactly for OpenAPI tooling such asaide.
schemars and utoipa compose — enable both while migrating between them.
OpenAPI responses with utoipa #
With the utoipa feature (on by default), the derive also implements
utoipa::IntoResponses:
one response per distinct status code (variants sharing a status are merged),
described by the variants' doc comments (one per line), with application/json
content using the enum's own schema:
#[derive(Error, Debug, Serialize, ToSchema, ErrorStatus)]
pub enum UserCreateError {
/// A user with this name already exists.
#[error("User {0} already exists")]
#[status(StatusCode::CONFLICT)]
UserAlreadyExists(String),
/// An unexpected error occurred.
#[error("internal error")]
#[status(500)]
Internal,
}
#[utoipa::path(post, path = "/users", responses(UserCreateError))]
async fn create_user() -> Result<(), UserCreateError> { /* ... */ }
documents a 409 response ("A user with this name already exists.") and a 500
response, both with application/json content matching the serialized enum.
With utoipa's auto_into_responses feature the error responses are picked up
from the handler's return type without listing them in responses(...).
Plain-text responses #
Disable default features to simply implement IntoResponse with the variant's
status code and the plain-text #[error(...)] message as the body — no extra
derives needed:
[dependencies]
axum_thiserror = { version = "0.3", default-features = false }
Wrapping foreign errors (sqlx::Error, std::io::Error, ...) #
Wrapped error types usually implement neither Serialize nor ToSchema. Serialize
them as their message string with #[serde(serialize_with)] and override the schema
with #[schema(value_type = String)] on the variant (with the schemars feature,
add #[schemars(with = "String")] on the field):
fn as_string<T: std::fmt::Display, S: serde::Serializer>(v: &T, s: S) -> Result<S::Ok, S::Error> {
s.collect_str(v)
}
#[derive(Error, Debug, Serialize, ToSchema, ErrorStatus)]
pub enum ApiError {
#[error("database error: {0}")]
#[status(StatusCode::INTERNAL_SERVER_ERROR)]
#[schema(value_type = String)]
Database(
#[from]
#[serde(serialize_with = "as_string")]
sqlx::Error,
),
}
which responds with a body like:
{ "Database": "error returned from database: relation \"users\" does not exist" }
License #
This project is licensed under the MIT License.