🎓 A1Learner
🔍

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 field

Hand 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?