Macro Diagnostics
Macro diagnostics are the compile-time error messages and source-code underlines (spans) emitted when a macro receives invalid syntax, unsupported types, or violated constraints. High-quality macro diagnostics use compile_error!("...") and span-preserving token generation (quote_spanned! / syn::Error::new_spanned) instead of panicking inside procedural macros.
Emitting Custom Compile Errors with compile_error!
The built-in compile_error!("message") macro halts compilation at macro-expansion time and prints "message" as a first-class compiler error—without exposing internal macro implementation noise.
In macro_rules!, a catch-all error arm at the bottom of a macro (or inside an unsupported configuration branch) guides users when they pass invalid syntax:
// ❌ does not compile
macro_rules! define_port {
(http => $port:literal) => {
const PORT: u16 = $port;
};
(https => $port:literal) => {
const PORT: u16 = $port;
};
// Diagnostic fallback rule for unsupported protocol names:
($other:ident => $port:literal) => {
compile_error!(concat!(
"unsupported protocol `",
stringify!($other),
"`; expected `http` or `https`"
));
};
}
define_port!(ftp => 21);
fn main() {}
error: unsupported protocol `ftp`; expected `http` or `https`
--> src/main.rs:11:9
|
11 | / compile_error!(concat!(
12 | | "unsupported protocol `",
13 | | stringify!($other),
14 | | "`; expected `http` or `https`"
15 | | ));
| |__________^
...
19 | define_port!(ftp => 21);
| ----------------------- in this macro invocation
Why Procedural Macros Should Return syn::Error::to_compile_error() Instead of panic!
Inside a procedural macro, calling panic!("invalid field") or .unwrap() produces a generic "proc-macro panicked" diagnostic that points to the top-level #[derive(...)] attribute rather than underlining the specific offending field or literal!
Using syn::Error::new_spanned(ast_node, "message").to_compile_error() emits a compile_error!("message") token stream whose Span is attached to the exact offending AST token:
`panic!("unsupported field")` in Proc Macro:
error: proc-macro derive panicked
--> src/main.rs:1:10
|
1 | #[derive(MyMacro)]
| ^^^^^^^ <-- Points only to the derive header!
`syn::Error::new_spanned(&field.ty, "unsupported field type").to_compile_error()`:
error: unsupported field type
--> src/main.rs:5:12
|
5 | count: f64,
| ^^^ <-- Underlines the exact offending field type!
Preserving Spans for Type-Checker Errors with quote_spanned!
When a procedural macro generates a trait method call on each struct field (for example, Serialize::serialize(&self.field)), using plain quote! { ... } attaches Span::call_site() to the generated call. If self.field fails to implement Serialize, the compiler underlines #[derive(Serialize)].
Using quote::quote_spanned! { field.span() => ... } attaches the field's source span to the generated trait call so rustc's type-checker error points directly to the specific field declaration that failed the trait bound!
// Inside a procedural derive macro using `quote::quote_spanned!`:
/*
use syn::spanned::Spanned;
let checks = fields.iter().map(|f| {
let ty = &f.ty;
// Attaches `f.ty`'s source span to the trait assertion:
quote::quote_spanned! { ty.span() =>
const _: fn() = || {
fn assert_impl<T: ::std::fmt::Display>() {}
assert_impl::<#ty>();
};
}
});
*/
fn main() {
println!("quote_spanned! attaches exact source coordinates to generated AST tokens.");
}
quote_spanned! attaches exact source coordinates to generated AST tokens.