When macros are the right tool in Clojure, how expansion works, and how to avoid turning metaprogramming into unreadable code.
A macro in Clojure is a form transformer: it receives unevaluated code, rewrites that code into a new form, and returns the form that will later be evaluated.
That power is why macros are both useful and dangerous. They let you introduce new control constructs, concise DSL-like syntax, and compile-time code generation. They also make code harder to read when they hide evaluation order, create surprising bindings, or solve problems that an ordinary function could handle more simply.
The best default rule is still: if a function works, use a function.
Use a macro when you need one of these:
Do not use a macro just because the body feels repetitive. Many repetitive patterns are better solved with higher-order functions, plain data, or helper functions.
A function receives values:
1(defn log-value [x]
2 (println "value =" x)
3 x)
A macro receives forms:
1(defmacro unless [condition & body]
2 `(if (not ~condition)
3 (do ~@body)))
That distinction matters because the macro can shape evaluation:
1(unless false
2 (println "executed"))
The body is not evaluated before the macro runs. The macro first rewrites the call into an if expression.
If a macro feels confusing, inspect the expanded result before doing anything else.
1(macroexpand-1
2 '(unless false
3 (println "hello")))
That usually reveals whether the macro is doing the right thing far faster than reading the macro body repeatedly.
macroexpand-1 is often enough when you want to see just one rewrite step. macroexpand is useful when nested macro layers are involved.
This habit matters because many macro bugs are not “runtime bugs” in the ordinary sense. They are expansion-shape bugs:
Most practical macros rely on syntax quote plus selective unquoting.
1(defmacro with-log [label expr]
2 `(let [result# ~expr]
3 (println ~label result#)
4 result#))
The parts matter:
` builds a code template~ inserts a value or form into that template~@ inserts a sequence of formsresult# uses auto-gensym so the macro does not collide with a caller’s local resultThat last point is not cosmetic. Binding hygiene is one of the main reasons macro code breaks in surprising ways.
Clojure macros are not hygienic by default in the full automatic sense found in some other languages. That means macro authors need to be deliberate.
A weak macro:
1(defmacro bad-twice [expr]
2 `(let [tmp ~expr]
3 [tmp tmp]))
If the caller already has a tmp binding, or if the macro grows more complex, subtle collisions can appear.
A safer version:
1(defmacro twice* [expr]
2 `(let [tmp# ~expr]
3 [tmp# tmp#]))
Use generated names when the macro introduces locals that belong to the expansion rather than the caller’s intent.
Hygiene also means avoiding repeated evaluation:
1(defmacro log-and-return [expr]
2 `(let [value# ~expr]
3 (println value#)
4 value#))
This is safer than expanding expr twice.
A good macro often has a small surface and pushes real work into ordinary functions.
For example:
1(defn validate-route-spec [spec]
2 (when-not (:path spec)
3 (throw (ex-info "Route spec requires :path" {:spec spec})))
4 spec)
5
6(defmacro defroute [name spec]
7 (let [validated (validate-route-spec spec)]
8 `(def ~name ~validated)))
This keeps the macro focused on shaping code while ordinary validation logic lives in testable function form. That is usually easier to debug and maintain than packing everything into one macro body.
Macros are strongest when they make the call site clearer.
Good macro use:
Weak macro use:
If a reader has to macroexpand almost every call site just to understand basic control flow, the macro layer is probably too heavy.
Before adding a macro, ask:
“Does this create a better language for the caller, or does it only move complexity out of sight?”
That question is useful because macros can make call sites look elegant while shifting the real cost into debugging, onboarding, and tooling.
flowchart TD
A["Need abstraction"] --> B{"Can a function or data structure express it?"}
B -->|Yes| C["Prefer function or data"]
B -->|No| D["Write small macro surface"]
D --> E["Check macroexpand-1 output"]
E --> F{"Readable and safe?"}
F -->|No| G["Refine expansion or remove macro"]
G --> E
F -->|Yes| H["Keep logic in plain functions where possible"]
This keeps metaprogramming tied to clarity rather than novelty.