ErrLookup › Background articles › syn::Error: Rust proc-macro compile errors — "expected string literal", "duplicate modifier", state-inference failures, and what attribute-macro errors really mean

syn::Error: Rust proc-macro compile errors — "expected string literal", "duplicate modifier", state-inference failures, and what attribute-macro errors really mean

syn::Error is the compile-time error that Rust procedural macros return when attribute arguments or the annotated item break the macro's rules — you meet it as a cargo check failure pointing at a specific token in an #[attribute] or #[derive], in libraries like axum, actix-web, tokio, dioxus, Rocket, fuels-rs, Graphite, and rustc's own macros. Typical messages include "expected string literal", "duplicate modifier", "can't infer state type", and "can only be derived for structs". This article explains the mechanism behind the whole family and the causes and fixes shared across libraries.

Distilled from 187 documented records across 8 repositories.

Background

syn is the crate Rust procedural macros use to parse token streams, and syn::Error is its error type: a message paired with a span. When a derive or attribute macro cannot parse its input, or parses it but decides it violates the macro's semantic rules, it returns a syn::Error instead of panicking, and the compiler renders it as a compile_error! diagnostic on that span. From the caller's side it looks like an ordinary rustc error aimed at a token inside your attribute or at the annotated item — which is why these errors surface at cargo check time, never at runtime.

The errors exist because attributes and derives are mini-languages layered on top of Rust: axum infers a router state type from State<T> arguments, rustc_queries! accepts a fixed block of query modifiers, Rocket's #[suppress] recognizes exactly five lint names, and fuels macros require comma-separated name="value" pairs. syn::Error is how each macro reports that its grammar or its semantic rules were violated. Presentation is library-specific: fuels remaps raw parse failures to a friendlier "expected name='value'" message, Graphite wraps inner failures in "Failed to parse node function:\n{e}" so the real cause is nested inside, and several macros embed the fix in the message itself — fuels suggests the exact Abigen line to add, and axum suggests the state = ... attribute to write.

Across the repositories the family splits into two broad shapes. Parse failures fire when a token has the wrong shape: an unquoted path in actix's #[get(/users)], a string where tokio's worker_threads wants an integer literal, a fuels argument given as a bare ident instead of a quoted literal, or a rustc desc block that does not start with a string literal. Semantic validations fire on input that parsed cleanly but breaks the macro's rules: axum cannot infer the state when two different State<T> types appear, actix rejects two multipart fields serializing to the same name, Graphite enforces structural rules on node functions and message enums, and rustc rejects a query modifier appearing twice. Span precision also varies by library — some errors mark the exact offending token, while others (such as rustc's desc diagnostics) point at the whole expression list.

A few members of the family depend on the build environment rather than the source text: rustc's current_version macro fails with a CFG_RELEASE error when cargo is invoked directly on a compiler crate without the environment that ./x.py bootstrap normally sets. Silent-versus-strict behavior is likewise library-specific — axum's TypedPath derive ignores attribute names other than typed_path, so a mis-spelled attribute surfaces only later as "Missing path", while dioxus deliberately rejects OpenAPI fields on plain #[route] so metadata is never silently dropped.

Common causes

What usually fixes it

Go deeper

Documented occurrences

…and 167 more across the corpus — use search.

Honest provenance: generated on 2026-08-16 from AI-assisted analysis of the linked records. See how records are made.