Errors: from panic! to syn::Error¶
Level: 301 · deep dive
One line: A derive that panics gives the user "proc-macro derive panicked" underlining the derive's name, with the reason in a help: line that a one-line view drops. Returning compile_error! makes the reason the error, still at the derive. A syn::Error built with new_spanned underlines the tokens that are wrong, and combine reports every mistake in one build.
| The macro | The error reads | It underlines | Mistakes per build |
|---|---|---|---|
| panics | proc-macro derive panicked, and the reason as help: message: |
the derive's name | the first |
returns compile_error!(…) |
the reason | the derive's name | the first, since this version returns early |
returns a spanned syn::Error |
the reason | the tokens that are wrong | every one, with combine |
One derive, three ways to refuse¶
#[derive(VariantName)] gives an enum a name() method. It only makes sense for variants that carry no data, so a variant like Custom(String) is a mistake the macro has to report. The crate reports it three ways:
//! `#[derive(VariantName)]`: `name()` returns a variant's name, for an enum
//! whose variants carry no data.
//!
//! A variant that does carry data is the user's mistake. The three derives
//! report it three ways, and are otherwise the same.
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::{Data, DeriveInput, Fields, Ident, Variant, parse_macro_input};
/// Way 1: panic.
#[proc_macro_derive(VariantNamePanics)]
pub fn variant_name_panics(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let Data::Enum(data) = &input.data else {
panic!("VariantName is for enums");
};
if let Some(variant) = data.variants.iter().find(|v| has_data(v)) {
panic!("{}", mistake(variant));
}
generate(&input.ident, data.variants.iter()).into()
}
/// Way 2: return a `compile_error!` instead of the impl.
#[proc_macro_derive(VariantNameCompileError)]
pub fn variant_name_compile_error(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let Data::Enum(data) = &input.data else {
return quote!(::core::compile_error!("VariantName is for enums");).into();
};
if let Some(variant) = data.variants.iter().find(|v| has_data(v)) {
let message = mistake(variant);
return quote!(::core::compile_error!(#message);).into();
}
generate(&input.ident, data.variants.iter()).into()
}
/// Way 3: a `syn::Error` for every mistake, each spanned on the wrong tokens.
#[proc_macro_derive(VariantName)]
pub fn variant_name(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
expand(&input)
.unwrap_or_else(syn::Error::into_compile_error)
.into()
}
/// Returning `syn::Result` is what lets every check below use `?`.
fn expand(input: &DeriveInput) -> syn::Result<TokenStream2> {
let Data::Enum(data) = &input.data else {
return Err(syn::Error::new_spanned(
&input.ident,
"VariantName is for enums",
));
};
check_variants(data.variants.iter())?;
Ok(generate(&input.ident, data.variants.iter()))
}
/// One error for every variant that carries data, combined into one `Err`.
fn check_variants<'a>(variants: impl Iterator<Item = &'a Variant>) -> syn::Result<()> {
let combined = variants
.filter(|v| has_data(v))
.map(|v| syn::Error::new_spanned(&v.fields, mistake(v)))
.reduce(|mut all, next| {
all.combine(next);
all
});
match combined {
Some(error) => Err(error),
None => Ok(()),
}
}
fn has_data(variant: &Variant) -> bool {
!matches!(variant.fields, Fields::Unit)
}
fn mistake(variant: &Variant) -> String {
let name = &variant.ident;
format!("`{name}` carries data; VariantName needs variants without fields")
}
fn generate<'a>(name: &Ident, variants: impl Iterator<Item = &'a Variant>) -> TokenStream2 {
let variants: Vec<&Ident> = variants.map(|v| &v.ident).collect();
quote! {
impl #name {
pub fn name(&self) -> &'static str {
match self {
#(Self::#variants => ::core::stringify!(#variants),)*
}
}
}
}
}
Used correctly:
use variant_name_derive::VariantName;
#[derive(VariantName)]
enum Method {
Get,
Post,
Delete,
}
fn main() {
for method in [Method::Get, Method::Post, Method::Delete] {
println!("{}", method.name());
}
}
Verified output of cargo run -q -p variant_name_app — declared in cargo_runs.toml and regenerated by tools/run_cargo_demos.py, never hand-typed.
The same mistake, reported three ways¶
This user added a variant that carries data. The three crates that make this mistake differ only in which derive they name; this one uses VariantNamePanics:
use variant_name_derive::VariantNamePanics;
#[derive(VariantNamePanics)]
enum Method {
Get,
Post,
Custom(String),
}
fn main() {
let method = Method::Custom("PURGE".to_string());
println!("{}", method.name());
}
1. panic!¶
Verified output of cargo build -q -p variant_name_by_panic, which fails on purpose — declared in cargo_runs.toml and regenerated by tools/run_cargo_demos.py, never hand-typed.
error: proc-macro derive panicked
--> reported_by_panic/src/main.rs:3:10
|
3 | #[derive(VariantNamePanics)]
| ^^^^^^^^^^^^^^^^^
|
= help: message: `Custom` carries data; VariantName needs variants without fields
error[E0599]: no method named `name` found for enum `Method` in the current scope
--> reported_by_panic/src/main.rs:12:27
|
4 | enum Method {
| ----------- method `name` not found for this enum
...
12 | println!("{}", method.name());
| ^^^^ method not found in `Method`
For more information about this error, try `rustc --explain E0599`.
error: could not compile `variant_name_by_panic` (bin "variant_name_by_panic") due to 2 previous errors
The compiler catches the panic and reports it at the only place it knows about, the derive's name. The reason is there, but as a help: line under a headline that says nothing about Custom. No location inside the macro crate is printed, and no backtrace hint: the key above was recorded on macOS and checked unchanged on Linux.
The second error follows from the first. The derive returned no impl, so name() does not exist, and every version on this page produces the same E0599. Re-emit the item on error is about the attribute-macro version of that follow-on error.
2. compile_error!¶
Verified output of cargo build -q -p variant_name_by_compile_error, which fails on purpose — declared in cargo_runs.toml and regenerated by tools/run_cargo_demos.py, never hand-typed.
error: `Custom` carries data; VariantName needs variants without fields
--> reported_by_compile_error/src/main.rs:3:10
|
3 | #[derive(VariantNameCompileError)]
| ^^^^^^^^^^^^^^^^^^^^^^^
|
= note: this error originates in the derive macro `VariantNameCompileError` (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0599]: no method named `name` found for enum `Method` in the current scope
--> reported_by_compile_error/src/main.rs:12:27
|
4 | enum Method {
| ----------- method `name` not found for this enum
...
12 | println!("{}", method.name());
| ^^^^ method not found in `Method`
For more information about this error, try `rustc --explain E0599`.
error: could not compile `variant_name_by_compile_error` (bin "variant_name_by_compile_error") due to 2 previous errors
The reason is now the error message itself, and the error is a normal compile error rather than a crashed macro. It still underlines the derive's name: quote! gives the tokens it writes Span::call_site() ↗, and for a derive the call site is its name inside #[derive(…)]. The note: says as much: this error originates in the derive macro. The macro also stops at the first mistake, as the panic did.
3. A spanned syn::Error¶
Verified output of cargo build -q -p variant_name_by_syn_error, which fails on purpose — declared in cargo_runs.toml and regenerated by tools/run_cargo_demos.py, never hand-typed.
error: `Custom` carries data; VariantName needs variants without fields
--> reported_by_syn_error/src/main.rs:7:11
|
7 | Custom(String),
| ^^^^^^^^
error[E0599]: no method named `name` found for enum `Method` in the current scope
--> reported_by_syn_error/src/main.rs:12:27
|
4 | enum Method {
| ----------- method `name` not found for this enum
...
12 | println!("{}", method.name());
| ^^^^ method not found in `Method`
For more information about this error, try `rustc --explain E0599`.
error: could not compile `variant_name_by_syn_error` (bin "variant_name_by_syn_error") due to 2 previous errors
The underline is on (String), the fields that should not be there, and the originates in the derive macro note is gone because the span is the user's own code. Error::new_spanned(&variant.fields, …) keeps the span of the first and last token ↗ it was given, here ( and ). into_compile_error ↗ then writes ::core::compile_error!{ "…" } with the path spanned at the first and the braces at the last, and rustc underlines everything between. So a syn::Error is a compile_error!, with spans taken from the input instead of the call site.
An editor that shows cargo check's diagnostics draws its underline from the same span as the carets: under VariantNamePanics and VariantNameCompileError inside #[derive(…)] for the first two, and under (String) for the third.
Every mistake in one build¶
A user with two mistakes, under the syn::Error version:
use variant_name_derive::VariantName;
#[derive(VariantName)]
enum Method {
Get,
Custom(String),
Post,
Numbered { code: u16 },
}
fn main() {
let method = Method::Custom("PURGE".to_string());
println!("{}", method.name());
}
Verified output of cargo build -q -p variant_name_two_mistakes, which fails on purpose — declared in cargo_runs.toml and regenerated by tools/run_cargo_demos.py, never hand-typed.
error: `Custom` carries data; VariantName needs variants without fields
--> two_mistakes/src/main.rs:6:11
|
6 | Custom(String),
| ^^^^^^^^
error: `Numbered` carries data; VariantName needs variants without fields
--> two_mistakes/src/main.rs:8:14
|
8 | Numbered { code: u16 },
| ^^^^^^^^^^^^^
error[E0599]: no method named `name` found for enum `Method` in the current scope
--> two_mistakes/src/main.rs:13:27
|
4 | enum Method {
| ----------- method `name` not found for this enum
...
13 | println!("{}", method.name());
| ^^^^ method not found in `Method`
For more information about this error, try `rustc --explain E0599`.
error: could not compile `variant_name_two_mistakes` (bin "variant_name_two_mistakes") due to 3 previous errors
check_variants makes one syn::Error per bad variant and folds them together with reduce, calling Error::combine ↗, which appends the other error's messages. into_compile_error then writes one compile_error! per message. syn also implements Extend<Error> for Error ↗ by calling combine, so first.extend(rest) does the same. The panic and compile_error! versions would have needed a second build to find Numbered.
The Result shape that makes ? work¶
The syn::Error version is written as two layers:
variant_name, the entry point, only converts:expand(&input).unwrap_or_else(syn::Error::into_compile_error).into().into_compile_errorhas the signaturefn(Error) -> proc_macro2::TokenStream, so it can be passed straight tounwrap_or_else.expandreturnssyn::Result<proc_macro2::TokenStream>, so each check ends in?and the firstErrreturns from the whole macro.syn::Result<T>isResult<T, syn::Error>↗.
parse_macro_input! ↗ is the same idea for parsing: on a parse error it returns err.to_compile_error(), converted to a proc_macro::TokenStream, from the function it is written in. That is why it goes in the entry point, and why expand takes an already-parsed DeriveInput.
In one line each¶
cargo build --message-format=short prints one line per diagnostic, location first:
Verified output of cargo build -q --message-format=short -p variant_name_by_panic, which fails on purpose — declared in cargo_runs.toml and regenerated by tools/run_cargo_demos.py, never hand-typed.
reported_by_panic/src/main.rs:3:10: error: proc-macro derive panicked
reported_by_panic/src/main.rs:12:27: error[E0599]: no method named `name` found for enum `Method` in the current scope: method not found in `Method`
error: could not compile `variant_name_by_panic` (bin "variant_name_by_panic") due to 2 previous errors
Verified output of cargo build -q --message-format=short -p variant_name_by_compile_error, which fails on purpose — declared in cargo_runs.toml and regenerated by tools/run_cargo_demos.py, never hand-typed.
reported_by_compile_error/src/main.rs:3:10: error: `Custom` carries data; VariantName needs variants without fields
reported_by_compile_error/src/main.rs:12:27: error[E0599]: no method named `name` found for enum `Method` in the current scope: method not found in `Method`
error: could not compile `variant_name_by_compile_error` (bin "variant_name_by_compile_error") due to 2 previous errors
Verified output of cargo build -q --message-format=short -p variant_name_by_syn_error, which fails on purpose — declared in cargo_runs.toml and regenerated by tools/run_cargo_demos.py, never hand-typed.
reported_by_syn_error/src/main.rs:7:11: error: `Custom` carries data; VariantName needs variants without fields
reported_by_syn_error/src/main.rs:12:27: error[E0599]: no method named `name` found for enum `Method` in the current scope: method not found in `Method`
error: could not compile `variant_name_by_syn_error` (bin "variant_name_by_syn_error") due to 2 previous errors
The panic's reason is gone: its line says proc-macro derive panicked and nothing else, because the reason was only ever a help: note. Only the syn::Error line's location, 7:11, is where the mistake is.
syn 2 and syn 3¶
Everything this page calls exists in syn 2 with the same behaviour. syn 3 changed the signatures of Error::new and Error::new_spanned from type parameters to impl Display and impl ToTokens arguments, which only breaks a call that names the types with a turbofish, and added Error::new_range, which spans a range of parse cursors. combine, to_compile_error and into_compile_error are unchanged.
If you are coming from another language¶
- C and C++.
#error "message"stops compilation with your message at the directive's own line, which is thecompile_error!version: the right words, the wrong place. C++'sstatic_assert(cond, "message")inside a template has the same problem, softened by the required from here chain GCC prints back to the user's line. - Python. A decorator or metaclass that raises while the class is being built gives a traceback whose last frame is inside the library, the panic version.
warnings.warn(message, stacklevel=2)is the closest thing tonew_spanned: it attributes the warning to the caller's line instead of the library's. Validators that collect every problem before raising, aspydanticdoes with aValidationErrorlisting each field, arecombine. - Java. An annotation processor that throws an exception crashes the compilation with a stack trace, the panic. The supported way is
Messager.printMessage(Diagnostic.Kind.ERROR, message, element): theelementargument places the error on the user's declaration, asnew_spannedplaces it on tokens, and a processor can report several before the round ends, which iscombine. - ABAP. (Not machine-checked — CI cannot run ABAP.)
MESSAGE … TYPE 'E'stops at the first problem, like the panic andcompile_error!versions. The application log (BAL_LOG_CREATE,BAL_LOG_MSG_ADD, then one display) collects every message and shows them together, which iscombine.
See also¶
- Every struct and enum shape — refusing a union with a spanned error
- Re-emit the item on error — the attribute-macro version, and the follow-on errors a missing item causes
- Testing with
trybuild— holding these messages to a recorded answer - Parsing with
syn— whereparse_macro_input!comes from unwrapis a TODO — the same argument against panicking, for ordinary code- Procedural macros — the chapter, in reading order
Po polsku¶
Ten sam błąd użytkownika — wariant enuma z danymi, Custom(String), tam gdzie makro obsługuje tylko warianty bez pól — zgłoszony na trzy sposoby, każdy z zapisanym komunikatem kompilatora. panic! daje nagłówek proc-macro derive panicked wskazujący nazwę makra, a przyczynę tylko w linijce help:, której w formacie --message-format=short w ogóle nie ma. compile_error! robi z przyczyny właściwy komunikat, ale nadal podkreśla nazwę makra, bo quote! nadaje tokenom Span::call_site(). syn::Error::new_spanned podkreśla dokładnie te tokeny, które są złe — tu (String) — a Error::combine pozwala zgłosić wszystkie pomyłki w jednej kompilacji.
Wygodny układ kodu: punkt wejścia tylko zamienia wynik, expand(&input).unwrap_or_else(syn::Error::into_compile_error), a funkcja expand zwraca syn::Result, więc każde sprawdzenie może kończyć się operatorem ?.
Szukaj po polsku: błędy w makrach proceduralnych · syn::Error::new_spanned · compile_error! · rust proc macro error handling · syn Error combine