24.2When to write a macro
Lesson 24.1 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:
// 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:
// 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:
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?
Show solution
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.
Question #2
Give one situation where a macro genuinely is the right tool, and one where a generic is the better answer instead.
Show solution
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.
Question #3
What does it mean that Rust's macros are "hygienic," and what bug does it prevent?
Show solution
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.
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) 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.