actix/actix-web · error · syn::Error

#[scope] macro must be attached to a module

Error message

#[scope] macro must be attached to a module

What it means

This compile-time error fires when the `#[scope("...")]` macro is attached to an item that is not a Rust module (`mod`). The macro attempts `syn::parse::<syn::ItemMod>(input)` at line 44 and if the annotated item is a function, struct, impl block, or anything else, parsing fails with this message.

Solutions

  1. Move the routes into a module and apply `#[scope]` to the module: `#[scope("/api")] mod api { ... }`
  2. If you need to scope a single handler, use the runtime `web::scope("/api").route(...)` API instead
  3. Ensure the annotated item is declared with `mod`, not `fn`, `struct`, etc.

Example fix

// before
#[scope("/api")]
async fn handler() -> impl Responder { ... }

// after
#[scope("/api")]
mod api {
    use actix_web::*;

    #[get("/users")]
    pub async fn users() -> impl Responder {
        HttpResponse::Ok()
    }
}
Defensive patterns

Strategy: type-guard

Type guard

// Compile-time error. #[scope(...)] must be on a module:
// #[scope("/api")]
// mod api { ... } — correct
//
// #[scope("/api")]
// fn handler() { ... } — incorrect (must be a module)

Prevention

When it happens

Trigger: Attaching `#[scope("/api")]` to a function, struct, enum, or impl block instead of a `mod` block. For example, `#[scope("/api")] async fn handler() { ... }` will fail because `scope` expects a module.

Common situations: Developers who confuse `#[scope]` (module-level macro) with `web::scope()` (runtime API), or who try to scope a single handler function instead of a group of routes in a module.

Related errors


AI-assisted analysis of actix/actix-web@4d435abc28 (2026-08-09). Data as JSON: /api/errors/c42410fb7e27174e. Report an issue: GitHub.

Appendix: source

Thrown at actix-web-codegen/src/scope.rs:45

            err.span(),
            "argument to scope macro is not a string literal, expected: #[scope(\"/prefix\")]",
        )
    })?;

    let scope_prefix_value = scope_prefix.value();

    if scope_prefix_value.ends_with('/') {
        // trailing slashes cause non-obvious problems
        // it's better to point them out to developers rather than

        return Err(syn::Error::new(
            scope_prefix.span(),
            "scopes should not have trailing slashes; see https://docs.rs/actix-web/4/actix_web/struct.Scope.html#avoid-trailing-slashes",
        ));
    }

    let mut module = syn::parse::<syn::ItemMod>(input).map_err(|err| {
        syn::Error::new(err.span(), "#[scope] macro must be attached to a module")
    })?;

    // modify any routing macros (method or route[s]) attached to
    // functions by prefixing them with this scope macro's argument
    if let Some((_, items)) = &mut module.content {
        for item in items {
            if let syn::Item::Fn(fun) = item {
                fun.attrs = fun
                    .attrs
                    .iter()
                    .map(|attr| modify_attribute_with_scope(attr, &scope_prefix_value))
                    .collect();
            }
        }
    }

    Ok(module.to_token_stream().into())
}

View on GitHub (pinned to 4d435abc28)