Rust Macro Errors with Spans
A spanned macro error is a compile error that carries a span, the source position to point at. A spanned error points at the exact tokens at fault.
Reject the wrong shape
Describe only accepts named fields. A tuple struct gets an error. The error takes its span from the fields:
Fields::Named(named) => &named.named,
other => {
return Err(syn::Error::new_spanned(
other,
"Describe needs named fields, like { a: i32 }",
))
}The error lands on the fields
The compiler underlines the whole field list. The underline sits on the user's line. It never points inside the macro:
#[derive(Describe)]
struct Pair(i32, i32);
// error: Describe needs named fields, like { a: i32 }Point at one field's type
The check walks each field in turn. A reference field fails the check. Its error takes the span of the type alone:
if let Type::Reference(_) = &field.ty {
return Err(syn::Error::new_spanned(
&field.ty,
"Describe cannot use a reference field",
));
}The underline is exact
Only the type is underlined here. The field name stays clean. So does the rest of the struct:
#[derive(Describe)]
struct Borrow<'a> { text: &'a str }
// error: Describe cannot use a reference fieldHand the error back
The entry point turns a failure into error tokens. The compiler then prints it like one of its own:
impl_describe(&ast)
.unwrap_or_else(syn::Error::into_compile_error)
.into()Example
// toolkit holds the Describe derive.
// Built and run with syn 2.0.119, quote 1.0.47, proc-macro2 1.0.107.
use toolkit::Describe;
#[derive(Describe)]
struct Player {
name: String,
score: u32,
}
fn main() {
println!("{}", Player::describe());
}Key points
- A syn::Error pairs a message with a span
- The span says where the compiler should point
- Error::new builds an error on a span you choose
- Error::new_spanned reads the span from a piece of parsed code
- to_compile_error() turns the error into tokens
- The tokens hold a compile_error! call
- Your span rides inside it
- A derive entry point cannot return a Result
- It must return a TokenStream
- So the helper returns the Result
- The entry point unwraps it with unwrap_or_else
- parse_macro_input! already reports parse failures this way
- You hand-build errors only for checks of your own
- The Writing a Derive Macro lesson panicked on bad input.
In this example, Describe runs both checks first. The struct has named fields. The shape check passes. No field is a reference. The type check passes. The macro then builds one method. The method joins each field name with its type. Main prints the result. Now feed the same macro a tuple struct. The build stops instead. The error names the rule. It underlines the fields. Nothing about the good path changes.
Note
A panic inside a macro still works
But it reads like a crash
A spanned error reads like a compiler message
Users trust the second one
Try it Yourself
Edit the code, then press Run.
Running is simulated · Rust Playground
Exercise
Why does the derive entry point use unwrap_or_else instead of returning the Result?