Browse Learn Clojure Foundations as a Java Developer

Prevent Variable Capture in Clojure Macros

Learn how Clojure macro hygiene protects caller code from accidental name collisions, and when to use auto-gensyms, explicit gensyms, or intentional caller bindings.

Macro hygiene means the names introduced by a macro should not accidentally collide with names in the caller’s code. Java developers usually rely on lexical scope and compiler checks. Clojure macro authors must also think about names created in generated code.

The rule is simple: if a name is only for the macro’s internal implementation, make it generated and unique.

How Capture Happens

This macro builds its expansion manually and introduces ordinary local names:

1(defmacro bad-benchmark [expr]
2  (list 'let ['start '(System/nanoTime)
3              'value expr]
4        (list 'println
5              "elapsed"
6              (list '- '(System/nanoTime) 'start))
7        'value))

The expansion introduces start before the caller expression runs. If the caller expression also uses start, the macro’s local can capture that reference:

1(let [start 10]
2  (bad-benchmark (+ start 5)))

The call site looks as if start means 10, but the generated code binds a new start first. That is the kind of bug that makes macros feel unpredictable.

Use Generated Locals

Use generated symbols for macro-owned locals:

1(defmacro benchmark [expr]
2  `(let [start# (System/nanoTime)
3         value# ~expr]
4     (println "elapsed"
5              (- (System/nanoTime) start#))
6     value#))

Symbols ending in # inside a syntax-quoted form become unique generated symbols. The caller’s start remains the caller’s local; the macro’s start# is private to the expansion.

Hygiene Decision Table

Name in expansion Recommended treatment
Internal temporary value. Use name# or an explicit gensym.
Caller-chosen binding name. Accept a symbol from the caller and document that the macro binds it intentionally.
Core function or special form. Let syntax quote qualify it, such as clojure.core/let.
Symbol intended to resolve in caller scope. Be explicit and rare; this creates an anaphoric macro style that teams must understand.

Intentional Bindings Are Different

Some macros intentionally introduce a name chosen by the caller:

1(with-open [reader (clojure.java.io/reader path)]
2  (.readLine reader))

That is not accidental capture. The caller explicitly supplies reader as a binding name. The danger is introducing hidden names the caller did not choose.

Macro Hygiene Review

Review question Why it matters
Does the macro introduce a local that the caller did not name? It should usually be a gensym.
Does the body run inside a binding introduced by the macro? Caller references may resolve differently than expected.
Does the macro intentionally expose a binding name? Document that call-site contract clearly.
Can macroexpand-1 show the generated names clearly? Review the expansion before trusting the macro.

Knowledge Check

### What is macro hygiene mainly trying to prevent? - [x] Accidental name collisions between generated code and caller code. - [ ] Runtime reflection warnings. - [ ] Java checked exceptions. - [ ] Tail-call optimization failures. > **Explanation:** Hygiene is about names in generated code. Macro-owned names should not accidentally capture or shadow caller names. ### When should you use an auto-gensym such as `value#`? - [x] When the macro needs an internal temporary local. - [ ] When the caller must supply a public binding name. - [ ] Whenever a Java method is called. - [ ] Only when the macro has no arguments. > **Explanation:** Auto-gensyms are ideal for hidden implementation locals that should be unique in each expansion. ### What makes a macro binding intentional rather than accidental capture? - [x] The caller explicitly supplies or agrees to the binding name as part of the macro contract. - [ ] The binding name is short. - [ ] The macro is in a separate namespace. - [ ] The macro uses Java reflection. > **Explanation:** Macros such as binding forms can intentionally introduce caller-visible names, but the contract must be obvious at the call site.
Revised on Saturday, May 23, 2026