---
title: "24.2 When to write a macro"
description: "Choosing a macro only when a function or generic can't do the job, plus a first look at macro hygiene."
url: "https://learnrust.net/chapter-24/when-to-write-a-macro/"
last_updated: "2026-06-13"
---

# 24.2 When to write a macro

Lesson [24.1](https://learnrust.net/chapter-24/declarative-macros/) showed *how* to write a `macro_rules!` macro. This one is about *whether* you should, which matters more, because the most common macro mistake is reaching for one when a plain function or a generic would have been simpler, clearer, and easier to debug. Macros are powerful, and that power costs readability: cryptic definitions, error messages that point at generated code, and a tool most Rust programmers read far better than they write. The guiding principle is to use a macro only when ordinary tools genuinely can't do the job.

## First ask: can a function do it?

Most of the time, the answer is yes, and then you should write the function. A function is easier to read, easier to debug (errors point at *your* code, not expanded code), shows up properly in documentation, and is bound by clear rules. If your would-be macro takes a fixed number of arguments and just computes something, it's a function wearing the wrong clothes:

```rust
// Don't do this:
macro_rules! square {
    ($x:expr) => { $x * $x };
}

// Do this:
fn square(x: i32) -> i32 {
    x * x
}
```

The macro version gains nothing and loses type checking, clarity, and good errors. `square` takes one argument and returns one value, the definition of a function's job. Reaching for a macro here is over-engineering.

## Then ask: can a generic do it?

The next temptation is "but my function would need to work for many *types*." That's not a reason for a macro, that's what **generics** (chapter 15) are for. If you want one piece of logic to apply across types, a generic function with trait bounds is the right tool, and again it keeps full type checking and clean errors:

```rust
// Not a macro, a generic:
fn largest<T: PartialOrd>(list: &[T]) -> &T {
    let mut biggest = &list[0];
    for item in list {
        if item > biggest {
            biggest = item;
        }
    }
    biggest
}
```

The standing `largest` example from chapter 15 needed to work for any orderable type, and generics handled it. A macro would have been a worse answer: no type checking until expansion, worse errors, harder to read. So the second filter: if the variation is across *types*, use a generic, not a macro.

## So when IS a macro the answer?

A macro earns its place only when you need something functions and generics structurally cannot provide. The honest, short list:

**A variable number of arguments.** A function's signature fixes its argument count; `println!("{} {}", a, b)` and `println!("{}", a)` need a macro because the count varies. This is the most common legitimate case.

**Generating repetitive code.** When you'd otherwise write near-identical implementations many times, a macro can stamp them out. Implementing the same trait for ten different types with one macro invocation per type saves real duplication that no function could.

**New syntax or compile-time construction.** `vec![0; 100]`, a custom domain-specific mini-language, building something from a syntactic pattern, these live outside what a function call can express.

**Operating on code itself.** Generating tests, deriving behavior from a type's structure, anything that needs to *inspect or transform the source*. (The most powerful version of this is procedural macros, the next lesson.)

If your need isn't on a list like this, a function or generic is almost certainly the better tool. The macro's complexity is only worth paying when it buys a capability the simpler tools genuinely lack.

> **Best practice**
>
> Default to a function. If the logic must span many types, use a generic. Reach for a macro only when you need a capability neither can provide, chiefly a variable number of arguments, generating repetitive code across many types, or constructing new syntax. The cost of a macro is real (cryptic definition, errors that point at generated code, a tool that's harder to maintain), so it should buy you something a function or generic structurally cannot. "Could this be a function?" is the question to ask first, every time.

## Macro hygiene, in passing

One reassuring property worth knowing, because it prevents a class of bug that plagues macros in older languages like C. Rust's macros are **hygienic**: identifiers a macro introduces don't accidentally collide with identifiers in the code that calls it. Recall the `my_vec!` macro from last lesson used a variable named `temp` internally. You'd worry: what if *I* have a variable called `temp` where I call the macro? In C, the macro's `temp` would clash with yours and silently corrupt your value. In Rust it can't:

```rust
macro_rules! my_vec {
    ( $( $x:expr ),* ) => {{
        let mut temp = Vec::new();
        $( temp.push($x); )*
        temp
    }};
}

fn main() {
    let temp = "my own temp";        // same name as the macro uses internally
    let v = my_vec![1, 2, 3];        // macro's internal `temp` does NOT clash
    println!("{temp}");
    println!("{v:?}");
}
```

```
my own temp
[1, 2, 3]
```

Your `temp` and the macro's `temp` are kept entirely separate by the compiler, so the macro can't reach out and stomp on your variables, and you can't accidentally interfere with its internals. This **hygiene** is automatic and is a major reason Rust's macros are safe to use in ways C's preprocessor macros never were. You get the code-generation power without the footgun of name collisions.

## Quiz time

**Question #1**

You're tempted to write a macro that takes one number and returns its cube. Should you? What's the better tool?

<details class="solution">
<summary>Show solution</summary>

No, write a function: `fn cube(x: i32) -> i32 { x * x * x }`. It takes a fixed number of arguments and computes a value, which is exactly a function's job. A macro here gains nothing and loses type checking, readability, and good error messages. Macros are for things functions *can't* do; a fixed-arity computation isn't one of them.

</details>

**Question #2**

Give one situation where a macro genuinely is the right tool, and one where a generic is the better answer instead.

<details class="solution">
<summary>Show solution</summary>

Macro: when you need a *variable number of arguments* (like `println!`/`vec!`) or to *generate repetitive code* (e.g. implementing a trait for many types with one invocation each), things a function's fixed signature can't express. Generic: when one piece of logic should work across *many types* (like `largest<T: PartialOrd>`), a generic function with trait bounds is better than a macro, keeping full type checking and clean errors. Variation in argument *count* or *code shape* → macro; variation in *type* → generic.

</details>

**Question #3**

What does it mean that Rust's macros are "hygienic," and what bug does it prevent?

<details class="solution">
<summary>Show solution</summary>

Hygiene means identifiers a macro introduces internally are kept separate from identifiers in the calling code, so they can't accidentally collide. If a macro uses a variable named `temp` internally and you also have a `temp` where you call it, the two don't interfere. This prevents the name-collision bugs that plague C's preprocessor macros (where the macro's names can silently clash with and corrupt the caller's), making Rust macros far safer to use.

</details>

You can write declarative macros and judge when to. But `macro_rules!` is only half the macro system, and the less powerful half. The next lesson ([24.3](https://learnrust.net/chapter-24/procedural-macros-overview/)) surveys **procedural macros**, the code-generating machinery behind `#[derive(Debug)]`, `#[tokio::main]`, and serde, which you'll read constantly and (for a long while) write never.

## Sitemap

See the full [sitemap](https://learnrust.net/sitemap.md) for all pages.
