How does quote::quote_spanned! preserve span information for better compiler error messages in proc macros?

quote::quote_spanned! assigns a specific source code span to the generated tokens, ensuring that compiler errors, warnings, and suggestions point to meaningful locations in the user's code rather than to opaque macro invocation sites. In procedural macros, code is generated programmatically, and by default the generated tokens have spans pointing to the macro definition site, which produces unhelpful error messages like "error inside procedural macro." quote_spanned! solves this by allowing macro authors to attach spans from input tokens to output tokens, so when the generated code has an error, the compiler can point to the exact location in the user's source that triggered the issue.

The Problem: Opaque Error Locations

use proc_macro::TokenStream;
use quote::quote;
 
#[proc_macro_derive(MyTrait)]
pub fn derive_my_trait(input: TokenStream) -> TokenStream {
    // Default quote! gives generated code a "call site" span
    // Errors point to the macro invocation, not the actual source
    let expanded = quote! {
        impl MyTrait for #input {
            fn method(&self) -> i32 {
                // If user's type has an error, message points here
                // instead of to the user's code
                self.nonexistent_field  // Error points to macro
            }
        }
    };
    
    expanded.into()
}

Default quote! uses Span::call_site(), producing generic error locations.

What Are Spans?

use proc_macro2::Span;
 
// A Span represents a location in source code:
// - File path
// - Line number
// - Column start/end
// - Parent span (for macro expansion context)
 
// Spans are used for:
// - Error messages: "error at line 10, column 5"
// - IDE features: go to definition, hover info
// - Macro hygiene: variable capture rules
 
fn span_explanation() {
    // Span::call_site(): Points to where macro was invoked
    // Span::mixed_site(): Hybrid of call site and definition site
    // Span::def_site(): Points to where macro is defined
    
    // quote_spanned! allows using SPANS from input tokens
    // so errors point to user's code, not macro internals
}

Spans carry source location information that the compiler uses for diagnostics.

Basic quote_spanned! Usage

use proc_macro::TokenStream;
use quote::{quote, quote_spanned};
use syn::{parse_macro_input, DeriveInput};
 
#[proc_macro_derive(Validate)]
pub fn derive_validate(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    
    // Get the span from the input type name
    let type_name = &input.ident;
    let type_span = type_name.span();
    
    // Use quote_spanned! with that span
    let expanded = quote_spanned! { type_span =>
        impl Validate for #type_name {
            fn validate(&self) -> Result<(), String> {
                // Errors in this impl block will point to
                // the user's type definition, not the macro
                Ok(())
            }
        }
    };
    
    expanded.into()
}

quote_spanned! assigns the specified span to all generated tokens.

Error Message Comparison

use proc_macro::TokenStream;
use quote::{quote, quote_spanned};
use syn::{parse_macro_input, DeriveInput, Data, Field};
 
#[proc_macro_derive(DebugCustom)]
pub fn derive_debug_custom(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let name = &input.ident;
    
    // WITHOUT quote_spanned - poor error location
    let poor = quote! {
        impl std::fmt::Debug for #name {
            fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
                // Error: "missing field `unknown_field` in MyStruct"
                // Points to macro invocation, not to struct definition
                write!(f, "MyStruct {{ unknown_field: {:?} }}", self.unknown_field)
            }
        }
    };
    
    // WITH quote_spanned - error points to user's code
    let good = quote_spanned! { name.span() =>
        impl std::fmt::Debug for #name {
            fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
                // Error points to `name` in user's source
                write!(f, "{} {{ ... }}", stringify!(#name))
            }
        }
    };
    
    good.into()
}

With quote_spanned!, errors point to the specific token that provided the span.

Attaching Spans from Field Names

use proc_macro::TokenStream;
use quote::quote_spanned;
use syn::{parse_macro_input, DeriveInput, Data, Fields, Field};
 
#[proc_macro_derive(Builder)]
pub fn derive_builder(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let name = &input.ident;
    
    let fields = match &input.data {
        Data::Struct(data) => {
            match &data.fields {
                Fields::Named(fields) => &fields.named,
                _ => panic!("Only named fields supported"),
            }
        }
        _ => panic!("Only structs supported"),
    };
    
    // Generate setter methods with proper spans
    let setters = fields.iter().map(|field| {
        let field_name = &field.ident;
        let field_ty = &field.ty;
        let span = field_name.as_ref().unwrap().span();
        
        // Each setter gets the span of its field
        quote_spanned! { span =>
            fn #field_name(&mut self, value: #field_ty) -> &mut Self {
                self.#field_name = Some(value);
                self
            }
        }
    });
    
    let expanded = quote_spanned! { name.span() =>
        impl Builder for #name {
            #(#setters)*
        }
    };
    
    expanded.into()
}

Each field's setter method gets that field's span for targeted error messages.

Error Pointing to Field Definition

use proc_macro::TokenStream;
use quote::quote_spanned;
use syn::{parse_macro_input, DeriveInput, Data, Fields};
 
#[proc_macro_derive(Serialize)]
pub fn derive_serialize(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let name = &input.ident;
    
    let fields = match &input.data {
        Data::Struct(data) => match &data.fields {
            Fields::Named(fields) => &fields.named,
            _ => panic!("Only named fields supported"),
        },
        _ => panic!("Only structs supported"),
    };
    
    let serialize_fields = fields.iter().map(|field| {
        let field_name = &field.ident;
        let field_span = field_name.as_ref().unwrap().span();
        
        // Error in field serialization points to field definition
        quote_spanned! { field_span =>
            // If field doesn't implement Serialize,
            // error points to field definition in struct
            #field_name: self.#field_name.serialize(),
        }
    });
    
    let expanded = quote_spanned! { name.span() =>
        impl Serialize for #name {
            fn serialize(&self) -> String {
                format!("{{ {} }}", [#(#serialize_fields),*].join(", "))
            }
        }
    };
    
    expanded.into()
}
 
// User's code:
// struct User {
//     name: String,
//     age: NonSerializable,  // Error points HERE
// }
//
// #[derive(Serialize)]
// struct User { ... }

Errors about field types point to the field definition in the struct.

Span Propagation Chain

use proc_macro::TokenStream;
use quote::quote_spanned;
use syn::{parse_macro_input, DeriveInput, Ident, Type};
 
#[proc_macro_attribute]
pub fn validate(_attr: TokenStream, item: TokenStream) -> TokenStream {
    let input = parse_macro_input!(item as DeriveInput);
    let name = &input.ident;
    
    // Span propagation chain:
    // 1. User writes `struct Foo { x: u32 }`
    // 2. `name` gets span of `Foo`
    // 3. Generated impl uses `name.span()`
    // 4. Errors point to `Foo` in user's code
    
    let name_span = name.span();
    
    let expanded = quote_spanned! { name_span =>
        impl Validate for #name {
            fn validate(&self) -> bool {
                true
            }
        }
    };
    
    expanded.into()
}
 
// Spans flow from input tokens through to output tokens:
// - Input: `struct MyStruct { ... }`
// - `MyStruct` token has a Span (file, line, column)
// - quote_spanned! attaches that Span to generated code
// - Errors reference that Span, pointing to `MyStruct`

Spans flow from input tokens to generated code, maintaining source location.

Different Span Sources

use proc_macro::TokenStream;
use quote::quote_spanned;
use syn::{parse_macro_input, DeriveInput, Data, Fields, Field};
 
#[proc_macro_derive(Validator)]
pub fn derive_validator(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let name = &input.ident;
    
    let fields = match &input.data {
        Data::Struct(data) => match &data.fields {
            Fields::Named(fields) => &fields.named,
            _ => panic!("Only named fields"),
        },
        _ => panic!("Only structs"),
    };
    
    // Different span sources for different purposes:
    
    // 1. Type name span - for impl block errors
    let type_span = name.span();
    
    // 2. Field name span - for field-specific errors
    let field_spans: Vec<_> = fields.iter()
        .map(|f| f.ident.as_ref().unwrap().span())
        .collect();
    
    // 3. Field type span - for type mismatch errors
    let type_spans: Vec<_> = fields.iter()
        .map(|f| f.ty.span())
        .collect();
    
    // Use appropriate span for each error context
    let validations = fields.iter().map(|field| {
        let field_name = field.ident.as_ref().unwrap();
        let field_ty = &field.ty;
        let field_span = field_name.span();
        
        // Field-specific validation with field span
        quote_spanned! { field_span =>
            if !self.#field_name.validate() {
                // Error points to this field in struct
                return false;
            }
        }
    });
    
    let expanded = quote_spanned! { type_span =>
        impl Validator for #name {
            fn validate(&self) -> bool {
                #(#validations)*
                true
            }
        }
    };
    
    expanded.into()
}

Different spans are appropriate for different error contexts.

Span for Generated Code vs Input Code

use proc_macro::TokenStream;
use quote::{quote, quote_spanned};
use syn::{parse_macro_input, DeriveInput};
 
#[proc_macro_derive(Clone)]
pub fn derive_clone(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let name = &input.ident;
    
    // For the impl block, use type span
    // This way, impl errors point to the type definition
    
    let name_span = name.span();
    
    // Regular quote! for internal implementation details
    // where we don't want to point to user's code
    let internal_code = quote! {
        // These spans point to macro definition
        // Acceptable for internal implementation
        fn clone(&self) -> Self {
            Self { /* ... */ }
        }
    };
    
    // quote_spanned! for user-facing parts
    let expanded = quote_spanned! { name_span =>
        impl Clone for #name {
            #internal_code
        }
    };
    
    expanded.into()
}

Use quote_spanned! for user-facing code, quote! for internal details.

Multiple Span Sources

use proc_macro::TokenStream;
use quote::quote_spanned;
use syn::{parse_macro_input, DeriveInput, Data, Fields};
 
#[proc_macro_derive(Accessors)]
pub fn derive_accessors(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let name = &input.ident;
    
    let fields = match &input.data {
        Data::Struct(data) => match &data.fields {
            Fields::Named(fields) => &fields.named,
            _ => panic!("Only named fields"),
        },
        _ => panic!("Only structs"),
    };
    
    let accessors = fields.iter().map(|field| {
        let field_name = field.ident.as_ref().unwrap();
        let field_ty = &field.ty;
        
        // Span for getter: points to field name
        let getter_span = field_name.span();
        
        // Span for return type: points to field type
        let type_span = field_ty.span();
        
        // Getter method
        let getter = quote_spanned! { getter_span =>
            pub fn #field_name(&self) -> &#field_ty {
                &self.#field_name
            }
        };
        
        // Setter method
        let setter = quote_spanned! { getter_span =>
            pub fn set_#field_name(&mut self, value: #field_ty) {
                self.#field_name = value;
            }
        };
        
        quote! {
            #getter
            #setter
        }
    });
    
    let impl_span = name.span();
    
    let expanded = quote_spanned! { impl_span =>
        impl #name {
            #(#accessors)*
        }
    };
    
    expanded.into()
}

Each generated method can use the span of its corresponding input element.

Hygiene and Macros

use proc_macro::TokenStream;
use quote::quote_spanned;
use proc_macro2::Span;
 
// Spans affect macro hygiene - which variables can be referenced
 
#[proc_macro]
pub fn create_variable(input: TokenStream) -> TokenStream {
    // Without proper spans, generated variables might:
    // - Shadow user variables unexpectedly
    // - Be inaccessible from user code
    // - Have confusing error messages
    
    let var_name = syn::Ident::new("internal_var", Span::call_site());
    let user_span = Span::call_site();  // Would use actual input span
    
    // Using call_site span means:
    // - Variable visible in same scope as macro call
    // - Can shadow user variables
    // - Errors point to macro call location
    
    let expanded = quote_spanned! { user_span =>
        let #var_name = 42;
    };
    
    expanded.into()
}
 
// Better: Use def_site span for internal variables
// This makes them "hygienic" - won't conflict with user code

Spans affect variable hygiene and scoping in macro-generated code.

Working with Error Types

use proc_macro::TokenStream;
use quote::quote_spanned;
use syn::{parse_macro_input, DeriveInput, parse::Error};
 
#[proc_macro_derive(TryFrom)]
pub fn derive_try_from(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let name = &input.ident;
    let name_span = name.span();
    
    // Compile-time errors with proper spans
    let expanded = match validate_derive(&input) {
        Ok(_) => {
            quote_spanned! { name_span =>
                impl std::convert::TryFrom<i32> for #name {
                    type Error = String;
                    
                    fn try_from(value: i32) -> Result<Self, Self::Error> {
                        Ok(Self {})
                    }
                }
            }
        }
        Err(err_msg) => {
            // Error points to the type that failed validation
            quote_spanned! { name_span =>
                compile_error!(#err_msg);
            }
        }
    };
    
    expanded.into()
}
 
fn validate_derive(input: &DeriveInput) -> Result<(), &'static str> {
    // Validation logic
    Ok(())
}

Use quote_spanned! with compile_error! for targeted compile-time diagnostics.

Practical Example: Builder Pattern

use proc_macro::TokenStream;
use quote::quote_spanned;
use syn::{parse_macro_input, DeriveInput, Data, Fields, Ident};
 
#[proc_macro_derive(Builder)]
pub fn derive_builder(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let name = &input.ident;
    let builder_name = Ident::new(&format!("{}Builder", name), name.span());
    
    let fields = match &input.data {
        Data::Struct(data) => match &data.fields {
            Fields::Named(fields) => &fields.named,
            _ => panic!("Only named fields"),
        },
        _ => panic!("Only structs"),
    };
    
    // Each field's optional type in builder
    let builder_fields = fields.iter().map(|field| {
        let field_name = field.ident.as_ref().unwrap();
        let field_ty = &field.ty;
        let span = field_name.span();
        
        // Error if field type doesn't support Option
        // will point to this field definition
        quote_spanned! { span =>
            #field_name: Option<#field_ty>
        }
    });
    
    // Build method
    let build_impl = quote_spanned! { name.span() =>
        pub fn build(self) -> Result<#name, &'static str> {
            // If fields missing, error points to struct
            Ok(#name {
                // Field initialization...
            })
        }
    };
    
    let expanded = quote_spanned! { name.span() =>
        pub struct #builder_name {
            #(#builder_fields),*
        }
        
        impl #builder_name {
            #build_impl
        }
    };
    
    expanded.into()
}

The builder struct and its methods get spans from the original struct definition.

Debugging Macro Spans

use proc_macro::TokenStream;
use quote::quote_spanned;
use syn::{parse_macro_input, DeriveInput};
 
#[proc_macro_derive(DebugSpan)]
pub fn derive_debug_span(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let name = &input.ident;
    
    // Span information for debugging
    let span = name.span();
    
    // Span provides:
    // - start line, column
    // - end line, column
    // - source file (in nightly/experimental)
    
    // Note: In stable Rust, you can't directly access
    // file/line info from Span, but the compiler uses it
    // for error messages
    
    // The span affects where errors point:
    // - call_site: macro invocation
    // - def_site: macro definition
    // - mixed_site: hybrid
    // - Custom span from input token: input location
    
    let expanded = quote_spanned! { span =>
        impl std::fmt::Debug for #name {
            fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
                write!(f, "{}", stringify!(#name))
            }
        }
    };
    
    expanded.into()
}

Spans carry location information used by the compiler for diagnostics.

When to Use quote_spanned! vs quote!

use proc_macro::TokenStream;
use quote::{quote, quote_spanned};
use syn::{parse_macro_input, DeriveInput};
 
#[proc_macro_derive(Example)]
pub fn derive_example(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let name = &input.ident;
    let name_span = name.span();
    
    // Use quote_spanned! for:
    // - impl blocks (point to type definition)
    // - Trait method errors (point to trait requirements)
    // - Field access errors (point to field definition)
    // - Type constraint errors (point to where clause)
    
    let impl_block = quote_spanned! { name_span =>
        impl Trait for #name {
            fn method(&self) -> i32 {
                42
            }
        }
    };
    
    // Use quote! for:
    // - Internal helper functions
    // - Macro implementation details
    // - Code that shouldn't surface errors to user
    // - Generated identifiers the user didn't write
    
    let helper = quote! {
        fn internal_helper() -> i32 {
            // Errors here point to macro, acceptable
            42
        }
    };
    
    let expanded = quote! {
        #impl_block
        #helper
    };
    
    expanded.into()
}

Use quote_spanned! for user-facing code, quote! for internal details.

Synthesis

Span basics:

// Spans carry source location:
// - File, line, column information
// - Used for error messages and IDE features
// - Default: Span::call_site() points to macro invocation
// - Custom spans from input tokens point to user's code
 
// quote! uses call_site span
let code = quote! { /* error points to macro call */ };
 
// quote_spanned! uses provided span
let code = quote_spanned! { user_span => /* error points to user's code */ };

When to use each:

// Use quote_spanned! when:
// - Generating impl blocks
// - Accessing user-defined fields
// - Creating methods for user types
// - Error messages should point to user's code
 
// Use quote! when:
// - Internal implementation details
// - Generated helper code
// - Code without user-facing errors
// - Performance-critical sections (slightly faster)

Error message impact:

// Without quote_spanned!:
// error[E0308]: mismatched types
//  --> src/main.rs:5:10
//   |
// 5 | #[derive(MyMacro)]
//   |          ^^^^^^^ expected `i32`, found `String`
//   |
// (Points to macro invocation, unhelpful)
 
// With quote_spanned!:
// error[E0308]: mismatched types
//  --> src/main.rs:7:5
//   |
// 7 |     field: String,  // expected `i32`
//   |     ^^^^^^^^^^^^^ expected `i32`, found `String`
//   |
// (Points to actual field, helpful)

Key insight: quote::quote_spanned! bridges the gap between generated code and user source code by attaching meaningful spans to generated tokens. Without explicit span management, all generated code has spans pointing to the macro invocation site, producing error messages that say "error in macro invocation" without context. With quote_spanned!, macro authors can propagate spans from input tokens to output tokens, so when the compiler encounters an error in generated code, it can point to the specific location in the user's source that caused the problem—a field that has the wrong type, a trait bound that isn't satisfied, or a struct that doesn't meet macro requirements. This transforms proc macros from opaque code generators into well-integrated language extensions that participate properly in the compiler's diagnostic system.