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
- Unsupported item shape.Most derives and attribute macros accept exactly one item shape: actix's MultipartForm needs structs with named fields, Graphite's node macro needs a plain fn, #[editor_commands] needs an inline module body, and TypedPath unit structs need capture-free routes. Anything else — an enum, a tuple or unit struct, an out-of-line mod, a non-fn item — is rejected at the annotated span.
- Wrong literal shape in attribute arguments.Attribute arguments must be literals of the exact expected kind: a quoted string for actix route paths and fuels name="value" pairs, an unquoted integer for tokio's worker_threads, and a string literal as the first element of rustc desc blocks. Constants, variables, bare identifiers, floats, and expressions are refused because proc macros only see literal tokens.
- Ambiguous or missing inference.axum's #[debug_handler] and #[derive(FromRequest)] infer the state type from State<T> arguments and fields; two different inner types make inference impossible and the macro asks for an explicit state = .... Similarly, FromRequest enums require an explicit via(...) wrapper and TypedPath requires a #[typed_path("...")] string.
- Misspelled names in a closed vocabulary.Some macros accept only a fixed identifier set: rustc query modifiers (eval_always, no_hash, ...), Rocket's five #[suppress] lint names, and fuels contract binding names in DeployContract. A typo, a missing underscore, or wrong casing yields an "unknown ..." error, and these macros usually list the valid names in the message.
- Duplicate keys or names.The same modifier twice in a rustc query block, two fields serializing to the same multipart name in actix (including through #[multipart(rename)]), or one #[hint] key repeated on a Graphite item each raise a duplicate error. Some macros report only the first duplicate per compile, so cleanup can take several passes.
- Disallowed content in macro-scoped containers.rustc_queries! permits only outer /// doc attributes on queries, and #[editor_commands] modules admit only use imports and private command functions. Extra items, visibility modifiers, or non-doc attributes are rejected because the macro regenerates the container and would otherwise silently drop them.
- Mixing incompatible macro flavors.dioxus ships method-named wrappers like #[get(...)] alongside the generic #[route(...)]; supplying the HTTP method in both places, or using plain #[route] with OpenAPI-only fields such as summary or tags, is a compile error rather than a silent ignore.
- Missing build environment.rustc's own macros read environment variables that ./x.py bootstrap sets; invoking cargo directly on a rustc crate without CFG_RELEASE fails at the macro call site. This is the rare family member caused by the environment rather than the source text.
What usually fixes it
- Read the span, then the innermost message. syn::Error points at the exact offending token, and wrapped errors such as Graphite's "Failed to parse node function:\n{e}" carry the real cause inside the wrapper — fix the inner diagnostic first.
- Match literal forms exactly. Quote strings, leave integers unquoted, and never pass consts, variables, or expressions to an attribute; check for stray suffixes, missing = signs, and trailing tokens in name=value argument lists.
- Conform the item to the shape the macro documents. Switch to named-field structs, plain fn items, or inline modules; give each route capture a corresponding field; wrap multi-field payloads in a struct. When the error prints the expected form (actix's #[<method>("<path>")]), copy it verbatim.
- Disambiguate explicitly instead of relying on inference. Set state = ..., via(...), or the path string explicitly; keep a single root State type and obtain sub-states through FromRef.
- Verify names against the macro's own vocabulary. Copy modifier spellings, lint names, and binding names from the error message (which usually lists them) or from the project's own tests and examples; remember that matching is often case-sensitive.
- Fix the first error, then re-run cargo check. Duplicates are reported one per pass and later spans often cascade from the first malformed token; pin regressions with trybuild UI tests, as several of these libraries do.
Go deeper
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Documented occurrences
- can't infer state type, please add set it explicitly, as in #[axum_macros::debug_{kind}(state = MyStateType)](tokio-rs/axum)
- missing #[from_request(via(...))](tokio-rs/axum)
- can't infer state type, please add #[{attr_name}(state = MyStateType)] attribute(tokio-rs/axum)
- Use `api_route` instead of `route` to use OpenAPI options(DioxusLabs/dioxus)
- CFG_RELEASE env var: {err}(rust-lang/rust)
- Expected a string literal(rust-lang/rust)
- Failed to parse value of `{field}` as integer.(tokio-rs/tokio)
- Validation error: {e}(GraphiteEditor/Graphite)
- attributes not supported on queries(rust-lang/rust)
- Missing path: #[typed_path("/foo/bar")](tokio-rs/axum)
- Consider adding: Contract(name="{}", project=...)(FuelLabs/fuels-rs)
- duplicate modifier(rust-lang/rust)
- attributes must be outer attributes (`///`), not inner attributes(rust-lang/rust)
- Typed paths for unit structs cannot contain captures(tokio-rs/axum)
- `MultipartForm` can only be derived for structs(actix/actix-web)
- unknown query modifier(rust-lang/rust)
- invalid lint `{name}` (known lints: {})(rwf2/Rocket)
- must have exactly one element(FuelLabs/fuels-rs)
- `MultipartForm` can only be derived for a struct with named fields(actix/actix-web)
- #[editor_commands] requires a module with an inline body(GraphiteEditor/Graphite)
…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.