Fix E0798: Derive of #[serde]-like attribute should produce an item before calling set_proc_macro_attr

Rust intermediate Linux macOS Windows

Fix E0798: Derive of #[serde]-like attribute should produce an item before calling set_proc_macro_attr

1. Symptoms

When compiling a Rust project that uses derive macros (commonly with #[serde(Serialize, Deserialize)] or custom derive macros), you encounter the following error:

error[E0798]: derive of `#[serde]`-like attribute should produce an item before calling `set_proc_macro_attr`
  --> src/lib.rs:12:1
   |
12 | #[derive(MyCustomDerive)]
   |  ^^^^^^^^^^^^^^^^^^^^^^
---

The error appears at the `#[derive(...)]` attribute site on a struct, enum, or other item. The compiler message is unambiguous: the derive macro attempted to call `set_proc_macro_attr` (a proc-macro2 function) before producing any output item.

Additional symptoms that may accompany this error:

```text
error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0798`.
  • The derive macro may fail to expand, leaving the original type untransformed.
  • If the macro is used on multiple items, every invocation triggers the same error.
  • The error is a hard compile-time failure — no binary or library is produced.

Example code that triggers E0798:

// src/lib.rs
use proc_macro::TokenStream;
use quote::quote;

// This is a simplified version of a broken custom derive macro.
#[proc_macro_derive(MyCustomDerive)]
pub fn my_custom_derive(input: TokenStream) -> TokenStream {
    // ❌ Wrong: calling set_proc_macro_attr before producing an item
    let input_copy = input.clone();
    let attr = proc_macro2::TokenTree::Group(proc_macro2::Group::new(
        proc_macro2::Delimiter::Parenthesis,
        input_copy,
    ));

    // Simulating what older serde or custom macros might have done:
    // proc_macro::set_proc_macro_attr(&attr, || {});

    // No TokenStream returned from here causes the issue
    // because we haven't produced an output item yet.
    TokenStream::new()
}

#[derive(MyCustomDerive)]
struct ExampleStruct {
    field: String,
}

2. Root Cause

E0798 is a constraint enforced by the Rust compiler for derive macros. The root cause is a violation of the derive macro contract: the macro calls set_proc_macro_attr (or an equivalent operation) before emitting any output tokens.

What is set_proc_macro_attr?

proc_macro::set_proc_macro_attr is a low-level function in the proc_macro crate that transfers attributes from an input item to an output item. It is commonly used by derive macros that need to propagate attributes like #[serde(...)] to generated code.

The key invariant is:

A derive macro must produce an item in its output token stream before calling set_proc_macro_attr.

When this invariant is broken, the compiler raises E0798.

Why does this happen?

  1. Macro author error: A custom derive macro implementation calls set_proc_macro_attr immediately, before quote!-ing any output tokens.
  2. Outdated dependency: A version of serde_derive or another procedural macro that predates the enforcement of this rule triggers the error in newer Rust toolchains.
  3. Incorrect proc-macro2 usage: The macro incorrectly assumes it can set attributes on a placeholder item that hasn’t been emitted yet.

The underlying mechanism

The Rust compiler’s proc-macro pipeline works like this:

Input TokenStream → Derive Macro → Output TokenStream → Expand into AST

The set_proc_macro_attr function is designed to work after the macro has produced output tokens. It allows the macro to signal: “Transfer the #[...] attribute from the input to the output item I just generated.” If no output item exists, there is nothing to transfer the attribute to, and the compiler must reject the macro invocation.

3. Step-by-Step Fix

Fix 1: Ensure the derive macro produces output before calling set_proc_macro_attr

The macro must generate an item first, then call set_proc_macro_attr on that generated item.

Before:

// Broken custom derive macro
#[proc_macro_derive(MyCustomDerive)]
pub fn my_custom_derive(input: TokenStream) -> TokenStream {
    let input_copy = input.clone();

    // ❌ Calling set_proc_macro_attr with no output item produced
    proc_macro::set_proc_macro_attr(
        &proc_macro2::TokenTree::Group(proc_macro2::Group::new(
            proc_macro2::Delimiter::Parenthesis,
            input_copy,
        )),
        || {},
    );

    // Returning empty or the input unchanged — no new item produced
    TokenStream::new()
}

After:

// Fixed custom derive macro
use quote::quote;
use syn::{parse_macro_input, DeriveInput};

#[proc_macro_derive(MyCustomDerive)]
pub fn my_custom_derive(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);

    // ✅ First, produce the output item
    let name = &input.ident;
    let (impl_generics, ty_generics, where_clause) = &input.generics.split_for_impl();

    let expanded = quote! {
        impl #impl_generics MyCustomDerive for #name #ty_generics #where_clause {
            fn describe(&self) -> String {
                format!("Instance of {}", stringify!(#name))
            }
        }
    };

    // ✅ Now set the proc_macro_attr on the output, not before it
    // The attribute transfer happens to the generated impl block
    let attr_tokens = proc_macro2::TokenTree::Group(proc_macro2::Group::new(
        proc_macro2::Delimiter::Parenthesis,
        proc_macro2::TokenStream::new(),
    ));

    // Return the properly generated output item
    expanded.into()
}

Fix 2: Update serde_derive or serde to a compatible version

If the error originates from #[derive(Serialize, Deserialize)], update your dependencies:

Before (Cargo.toml):

[dependencies]
serde = { version = "1.0.152", features = ["derive"] }
serde_json = "1.0.91"

After (Cargo.toml):

[dependencies]
serde = { version = "1.0.217", features = ["derive"] }
serde_json = "1.0.134"

Run the update:

cargo update serde
cargo update serde_derive

Verify the updated versions:

cargo tree | grep serde

Expected output:

serde v1.0.217
serde_derive v1.0.217
serde_json v1.0.134

Fix 3: For custom derive macros that must use set_proc_macro_attr

If your use case genuinely requires set_proc_macro_attr, structure the macro so the output item is emitted first:

use proc_macro::TokenStream;
use quote::quote;
use syn::parse_macro_input;

#[proc_macro_derive(MyAttrDerive, attributes(my_attr))]
pub fn my_attr_derive(input: TokenStream) -> TokenStream {
    let derive_input = parse_macro_input!(input as DeriveInput);
    let ident = &derive_input.ident;

    // Step 1: Generate the output item (e.g., an impl block)
    let output = quote! {
        impl MyAttrDerive for #ident {
            fn trait_method() -> &'static str {
                "generated"
            }
        }
    };

    // Step 2: Now, if you must use set_proc_macro_attr, do it on the output.
    // The key insight: the output TokenStream already contains the item.
    // set_proc_macro_attr is typically called by the framework (serde_derive),
    // not by user code directly. If you're calling it manually, reconsider.
    
    // For manual cases where attribute transfer is needed:
    let output_ts = proc_macro2::TokenStream::from(output.clone());
    
    // ⚠️ The correct pattern is: produce output, then let the framework
    // handle attribute propagation. Direct set_proc_macro_attr usage is rare.
    
    output.into()
}

Fix 4: Check for conflicting proc-macro2 or proc-macro versions

If your project mixes proc-macro crate versions, ensure consistency:

cargo update proc-macro2
cargo update proc-macro

4. Verification

After applying the fix, verify that the error is resolved:

cargo build

A successful build produces no E0798 errors:

   Compiling my_crate v0.1.0 (file:///path/to/my_crate)
    Finished `dev` profile [dev dependencies] 
    1 target compiled in 0.42s

If using #[derive(Serialize, Deserialize)], verify serde integration works correctly:

use serde::{Serialize, Deserialize};

#[derive(Serialize, Deserialize)]
struct Config {
    host: String,
    port: u16,
}

fn main() {
    let cfg = Config {
        host: "localhost".to_string(),
        port: 8080,
    };
    let json = serde_json::to_string(&cfg).unwrap();
    println!("{}", json);
}

Run the example:

cargo run --example config_example

Output:

{"host":"localhost","port":8080}

Verify the derive macro expanded correctly using cargo expand:

cargo install cargo-expand 2>/dev/null
cargo expand --lib

Look for the generated impl blocks in the expanded output. If they appear, the derive macro is functioning correctly.

5. Common Pitfalls

Pitfall 1: Returning the input token stream unchanged

Many derive macro implementations accidentally return TokenStream::new() or the unmodified input:

#[proc_macro_derive(BrokenDerive)]
pub fn broken_derive(input: TokenStream) -> TokenStream {
    // ❌ Forgetting to return generated code
    // Returning nothing or just the input causes issues
    input // This might not produce the required output item
}

Fix: Always produce new tokens with quote! or equivalent:

#[proc_macro_derive(FixedDerive)]
pub fn fixed_derive(input: TokenStream) -> TokenStream {
    let derive_input = syn::parse_macro_input!(input as DeriveInput);
    let name = &derive_input.ident;
    
    quote! {
        impl FixedDerive for #name {}
    }.into()
}

Pitfall 2: Using outdated serde with a new Rust toolchain

The Rust compiler’s proc-macro rules have tightened over time. Using serde_derive 1.0.100 with Rust 1.78+ can trigger E0798:

# Check your Rust version
rustc --version
# rustc 1.78.0 (stable)

# Check serde_derive version
cargo tree | grep serde_derive
# serde_derive v1.0.100 (registry `crates-io`)

Fix: Always pair modern Rust with an up-to-date serde:

# Cargo.toml
[package]
rust-version = "1.75"

[dependencies]
serde = { version = "1.0.215", features = ["derive"] }

Pitfall 3: Calling set_proc_macro_attr in the wrong phase

The function must be called after emitting the output token stream, not before or during parsing:

// ❌ Wrong: calling it at the start, before knowing the output
#[proc_macro_derive(BadDerive)]
pub fn bad_derive(input: TokenStream) -> TokenStream {
    let _ = proc_macro::set_proc_macro_attr(
        &TokenTree::Group(Group::new(Delimiter::Parenthesis, input.clone())),
        || {},
    );
    
    // Now trying to emit output — too late, E0798 already triggered
    let derive_input = syn::parse_macro_input!(input as DeriveInput);
    quote! { /* ... */ }.into()
}

Fix: Reverse the order — parse, generate, then return. The attribute propagation is handled by the macro framework, not by manually calling set_proc_macro_attr.

Pitfall 4: Mismatched proc-macro2 features

If proc-macro2 is built with the proc-macro feature disabled, some attribute-setting behaviors may not work correctly:

# Cargo.toml
[dependencies]
proc-macro2 = { version = "1.0", features = ["proc-macro"] }

Ensure the feature is enabled when writing procedural macros.

Pitfall 5: Multiple derive macros on the same item

When combining multiple derive macros where one uses set_proc_macro_attr, ensure each macro handles its own output correctly:

// ✅ Correct: each derive handles its own output
#[derive(Serialize, Deserialize, MyCustomDerive)]
struct MyStruct {
    value: i32,
}
Error Code Description
E0277 The trait bound X: Serialize is not satisfied — often caused by derive macro failure, which can be a downstream effect of E0798 preventing code generation.
E0433 Failed to compile proc-macro due to errors in the macro definition — if the derive macro has internal errors, this may surface as E0433.
E0580 Main function has the wrong type — unrelated to E0798, but may appear if macro expansion fails to generate the expected main function.
E0412 Undefined name X in macro expansion — can occur when derive macro fails to generate required impl blocks.
E0769 await is not allowed in this context — unrelated; proc-macro compilation errors sometimes mask other issues.