Debugging Macros
Macro bugs show up as confusing compiler errors at expansion sites. cargo expand, eprintln! in proc macros, and trybuild tests are the main debugging tools.
Recipe
cargo install cargo-expand
cargo expand -p mycrate path::to::function
RUSTFLAGS="-Z unstable-options" cargo expand --theme=monokai # optional// inside proc macro during dev:
eprintln!("parsed: {:?}", input);When to reach for this: Any time expanded code does not match mental model or errors reference wrong spans.
Working Example
# Expand derive output
cargo expand -p api handlers::create_order
# Expand specific module
cargo expand -p my_macros derive_validate
# Pretty print in test with macrotest// proc-macro crate dev
#[proc_macro_derive(MyTrait)]
pub fn derive_my_trait(input: TokenStream) -> TokenStream {
let ast = syn::parse_macro_input!(input as DeriveInput);
if std::env::var("DEBUG_MACRO").is_ok() {
eprintln!("{:#?}", ast);
}
// ...
TokenStream::new()
}What this demonstrates:
cargo expandshows post-macro Rusteprintln!in proc macro runs at compile timeDEBUG_MACROenv gate avoids noisy normal builds- Compare expansion across toolchain versions
Deep Dive
Error Quality
return syn::Error::new_spanned(&field.ident, "field must have #[validate]")
.to_compile_error()
.into();- Good spans beat good messages at wrong line
compile_error!inmacro_rules!fallback arm
trybuild
#[test]
fn ui() {
let t = trybuild::TestCases::new();
t.compile_fail("tests/ui/missing_field.rs");
}Gotchas
- Reading error without expand - wasted time. Fix: expand first habit.
- eprintln in published macro - noisy for users. Fix: feature flag
debug-macros. - Wrong
-ppackage - empty expand. Fix: workspacecargo expand -p proc_crate. - Huge expansion unreadable - hard to diff. Fix:
rustfmtexpanded output; snapshot withmacrotest. - Nightly-only expand features - CI on stable. Fix: document nightly optional tricks.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
trace_macros! (unstable) | macro_rules step trace | Stable CI |
macrotest snapshots | regression on expansion | Quick one-off |
Manual quote! in test | Unit test generator fn | Full integration |
FAQs
expand does not include std derives?
Some built-in expands differ; focus on your proc macros and macro_rules!.
How expand tokio::main?
cargo expand --bin myapp shows main transformation.
IDE expand support?
rust-analyzer "Expand macro recursively" for some macros; cargo-expand for full fidelity.
Why span is call_site?
Generated identifiers resolve at macro definition site hygiene; use deliberately.
macro_rules ambiguity debug?
Comment out arms; use compile_error!(stringify!($($t)*)) arm to see tokens.
CI test expansions?
macrotest or check in trybuild pass/fail only.
Expand all crate?
cargo expand -p crate dumps entire crate - large output.
proc macro panic?
Shows as compiler error with backtrace if RUST_BACKTRACE=1 on macro author machine.
Compare two toolchains?
Expand same input on 1.96 vs 1.97 when upgrading MSRV.
document debugging?
CONTRIBUTING for macro crate: expand command, trybuild, env flags.
Related
- macro_rules! - declarative macros
- syn & quote - AST debugging
- Useful Cargo Subcommands - expand install
- Macros Best Practices
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.