What Is Macro Hygiene in Rust?

Macro hygiene in Rust prevents naming conflicts between macro-generated code and the surrounding scope by treating identifiers as unique.

The macro that broke your variable

You write a macro to generate a debug helper. It works perfectly in main. You move it to a library crate. You use it in a function that has a local variable named temp. The macro suddenly starts reading temp from the function instead of creating its own. Or the macro creates a temp that overwrites your function's temp. The code compiles, but the logic is broken. You spent an hour hunting a bug that only appears when a specific variable name exists nearby.

This is the nightmare of text-based macros in C. The preprocessor blindly pastes text. If the macro uses i and your loop uses i, they collide. Rust has a shield against this called hygiene. Hygiene ensures that identifiers generated inside a macro belong to the macro's scope, not the caller's scope. The compiler tracks where every name comes from and refuses to mix them up.

Hygiene: Macros as functions, not text

In C, a macro is a text substitution. The preprocessor replaces the macro call with the macro body before the compiler sees the code. The compiler has no idea a macro was involved. It just sees the pasted text. If the text contains a variable name that conflicts with the surrounding code, the compiler complains or silently accepts a bug.

Rust macros are different. They are syntactic transformations. Hygiene makes macros behave like function calls. When you call a function, the function has its own local variables. It doesn't steal variables from the caller unless you pass them. Hygiene gives macros the same isolation. The compiler treats the macro body as a separate scope. Variables defined inside the macro stay inside the macro. Variables defined in the caller stay in the caller. The two scopes do not leak into each other.

Think of hygiene like a namespace that the compiler enforces automatically. You don't declare the namespace. You don't import it. The compiler creates it behind the scenes based on where the tokens originated.

macro_rules! print_x {
    () => {
        // This 'x' is born inside the macro definition.
        // The compiler tags it with a span pointing to this line.
        let x = 42;
        println!("Macro x: {}", x);
    };
}

fn main() {
    // This 'x' is born in main.
    // The compiler tags it with a span pointing to this line.
    let x = 10;

    // The macro expands here, but its 'x' stays separate.
    print_x!();

    // Main's 'x' is untouched.
    println!("Main x: {}", x);
}

The output is Macro x: 42 followed by Main x: 10. The macro's x and the main's x coexist peacefully. They have the same name, but the compiler knows they are different because they have different origins.

How the compiler tracks names

The compiler attaches metadata to every token called a Span. A span is a location marker that points to the source code where the token was defined. When you write a macro, the tokens in the macro body get spans pointing to the macro definition. When you write code in main, the tokens get spans pointing to main.

When the macro expands, the compiler inserts the macro's tokens into the call site. The tokens keep their original spans. The name resolver looks at the spans to decide which scope a name belongs to. If two names have different spans, the compiler treats them as distinct, even if they have the same text.

This mechanism is automatic. You cannot disable hygiene in macro_rules!. You cannot opt out. The compiler enforces it for every macro expansion. This design choice prevents an entire class of bugs. Macros become composable. You can use a macro inside another macro without worrying about name collisions. You can use a macro in a function with any variable names without breaking the macro.

Hygiene also applies to types and functions. If a macro defines a struct named Helper, that struct is local to the macro. The caller cannot access it. If the caller has a struct named Helper, the macro's Helper does not conflict.

Real code: Swapping without collisions

Hygiene shines when macros generate temporary variables. A common pattern is a macro that swaps two values. In C, you would write a macro that uses a temporary variable. If the caller has a variable with the same name, the macro breaks. In Rust, hygiene solves this instantly.

macro_rules! swap_values {
    ($a:expr, $b:expr) => {
        // 'temp' is defined here.
        // Hygiene ensures this 'temp' doesn't clash with a 'temp' in the caller.
        let temp = $a;
        $a = $b;
        $b = temp;
    };
}

fn main() {
    let mut a = 1;
    let mut b = 2;
    let temp = 999; // Caller has a 'temp'.

    swap_values!(a, b);

    // 'temp' is still 999. The macro's 'temp' was isolated.
    assert_eq!(temp, 999);
    assert_eq!(a, 2);
    assert_eq!(b, 1);
}

The macro introduces temp. The caller also has temp. Hygiene keeps them separate. The macro's temp is used for the swap and then dropped. The caller's temp remains unchanged. This code works regardless of what variables the caller defines. The macro is robust.

When hygiene bites back

Hygiene protects you from collisions, but it also restricts what macros can do. A macro cannot access variables from the caller unless you pass them as arguments. If you try to use a caller variable inside the macro body without passing it, the compiler rejects the code.

macro_rules! print_secret {
    () => {
        // Error: cannot find value `secret` in this scope
        // E0425
        println!("Secret: {}", secret);
    };
}

fn main() {
    let secret = 42;
    print_secret!();
}

The compiler emits E0425 (cannot find value). The macro body is isolated. It does not see secret from main. This is intentional. If macros could grab variables from the caller, they would be fragile and unpredictable. You would have to audit every macro to see what variables it might capture. Hygiene forces macros to be explicit. If a macro needs data, it must take arguments.

macro_rules! print_secret {
    ($val:expr) => {
        // $val is resolved in the caller's scope.
        // The caller passes the value explicitly.
        println!("Secret: {}", $val);
    };
}

fn main() {
    let secret = 42;
    print_secret!(secret);
}

Macro arguments are evaluated in the caller's scope. When you write $val, the compiler looks for val in the call site. This is the bridge between the macro and the caller. You pass data through arguments. You do not reach into the caller's scope.

Hygiene also hides macro internals from the caller. If a macro defines a variable, the caller cannot use it after the macro expands.

macro_rules! define_helper {
    () => {
        let helper = 5;
    };
}

fn main() {
    define_helper!();
    // Error: cannot find value `helper` in this scope
    // E0425
    println!("{}", helper);
}

The variable helper exists only inside the macro expansion. It is dropped when the macro block ends. The caller cannot access it. This keeps the macro's implementation details private.

Procedural macros and the hygiene gap

Procedural macros (proc_macro) have different hygiene rules. A procedural macro receives a token stream and returns a token stream. The compiler does not automatically enforce hygiene for procedural macros in the same way. The tokens generated by a procedural macro may inherit the hygiene context of the caller or the definition, depending on how they are constructed.

This makes procedural macros more powerful but also more dangerous. You can generate code that captures caller variables if you are not careful. You can also generate code that fails to resolve names because the hygiene context is wrong. The community generally prefers macro_rules! for transformations where hygiene matters. macro_rules! gives you hygiene for free. Procedural macros require you to manage hygiene manually or accept the risks.

When you use a procedural macro, expect that identifiers in the output may resolve in the caller's scope. If the macro generates a variable name, it might collide with the caller. If the macro uses a type name, it might fail to find it. You often need to use fully qualified paths or unique names to avoid issues.

Convention aside: The Rust community treats macro_rules! as the default choice for macros. Use macro_rules! whenever possible. Reach for procedural macros only when you need to parse arbitrary syntax or generate code based on complex analysis. The hygiene guarantee of macro_rules! is a significant advantage.

Inner macros and resolution

Hygiene has a special rule for inner macros. If your macro calls another macro, that inner macro resolves in the caller's scope, not the definition scope. This is intentional. It allows macros to use standard library macros like vec! or format! without requiring the caller to import them.

macro_rules! make_vec {
    () => {
        // vec! resolves in the caller's scope.
        // If the caller has `use std::vec::Vec;`, this works.
        vec![1, 2, 3]
    };
}

fn main() {
    // The caller provides vec!.
    make_vec!();
}

If vec! resolved in the definition scope, the macro would only work if the crate defining the macro imported vec!. Users would have no control. By resolving in the caller's scope, the macro delegates the import to the user. This makes macros more flexible.

Convention aside: This behavior is sometimes called local_inner_macros. Older documentation mentions local_inner_macros as a feature flag. In modern Rust, this behavior is the default for macro_rules!. You do not need to enable anything. Inner macros always resolve in the caller's scope.

Decision matrix

Use macro_rules! when you need a hygienic transformation. The compiler handles scope isolation automatically. You get safety without extra work.

Use macro arguments when the macro needs to operate on caller data. Hygiene blocks access to caller variables by design. Pass values explicitly through $name patterns.

Expect inner macros to resolve in the caller's scope. If your macro calls format! or vec!, it finds those macros where the user called your macro, not where you defined it. This is a feature, not a bug.

Reach for procedural macros when you need to parse arbitrary syntax or generate code based on complex analysis. Accept that hygiene is weaker. You may need to use fully qualified paths or unique names to avoid collisions.

Treat hygiene as a constraint. If your macro design requires leaking variables or capturing caller state, rethink the design. Macros should be self-contained transformations. Pass what you need. Hide what you generate.

Trust the spans. The compiler knows where names come from. If you get a name resolution error, check the spans. The error usually points to a hygiene boundary.

Where to go next