A derive macro that generates From<YourEnum> for tonic::Status from per-variant attributes.
Add the crate to your Cargo.toml:
[dependencies]
tonicerror = "0.1.0"
tonic = "0.14"Derive TonicError on an enum and annotate every variant with #[tonic_error(...)]:
use tonicerror::TonicError;
#[derive(TonicError)]
enum AppError {
#[tonic_error(code = tonic::Code::InvalidArgument, message = "invalid payload")]
Invalid,
#[tonic_error(code = tonic::Code::NotFound, message = "record {0} missing")]
Missing(u64),
#[tonic_error(code = tonic::Code::Internal, message = "storage {resource} broken")]
Storage { resource: String },
}The macro generates:
impl From<AppError> for tonic::Status {
fn from(value: AppError) -> Self { ... }
}All attributes go inside #[tonic_error(...)].
Required for non-transparent variants. Any expression that evaluates to tonic::Code.
#[tonic_error(code = tonic::Code::NotFound, message = "not found")]
Missing,Required for non-transparent variants. Supports std::format!-style placeholders:
| Variant kind | Placeholder syntax | Resolves to |
|---|---|---|
| Unit | — | (no placeholders) |
Tuple (A, B) |
{0}, {1}, or {} (auto-increments) |
fields positionally |
Struct { a, b } |
{a}, {b} |
fields by name |
#[tonic_error(code = tonic::Code::NotFound, message = "record {0} not found")]
Missing(u64),
#[tonic_error(code = tonic::Code::Internal, message = "{resource} failed")]
Storage { resource: String },Format specifiers are supported: {0:.2}, {name:?}, etc.
Delegates conversion to the inner type, which must itself implement Into<tonic::Status>. Only valid on single-field tuple variants.
#[tonic_error(transparent)]
Nested(NestedError),The inner status code and message are passed through unchanged.
When combined with message, the message becomes a format string where {} / {0} refers to the inner status message:
#[tonic_error(transparent, message = "request failed: {}")]
Nested(NestedError),If the inner status message is "forbidden resource", the output message will be "request failed: forbidden resource". The status code is still taken from the inner error.
Placing #[from] on the single field of a tuple variant generates an additional From<Inner> for Enum conversion:
#[tonic_error(transparent, message = "request failed: {}")]
Nested(#[from] NestedError),This lets you use ? or From::from to wrap the inner error:
let error: AppError = NestedError::Forbidden("x").into();#[from] is only supported on single-field tuple variants. Placing it on the variant itself (rather than the field) is a compile error.
use tonicerror::TonicError;
#[derive(TonicError)]
enum AppError {
#[tonic_error(code = tonic::Code::InvalidArgument, message = "invalid payload")]
Invalid,
#[tonic_error(code = tonic::Code::NotFound, message = "record {0} missing")]
Missing(u64),
#[tonic_error(code = tonic::Code::Internal, message = "storage {resource} broken")]
Storage { resource: String },
#[tonic_error(transparent, message = "downstream: {}")]
Downstream(#[from] DownstreamError),
}
#[derive(TonicError)]
enum DownstreamError {
#[tonic_error(code = tonic::Code::Unavailable, message = "service {0} unavailable")]
Unavailable(&'static str),
}
fn handler() -> Result<(), AppError> {
Err(DownstreamError::Unavailable("payments"))?
// AppError::from(DownstreamError::Unavailable("payments"))
// → tonic::Status { code: Unavailable, message: "downstream: service payments unavailable" }
}