Learn how macro expansion changes argument evaluation compared with Java method calls, and how to avoid repeated side effects in generated Clojure code.
A Java method receives evaluated argument values. A Clojure macro receives unevaluated forms and returns a new form that will be evaluated later. That one difference explains many macro bugs.
If a macro inserts the same caller form twice, that form can run twice. If the form mutates state, logs, reads I/O, allocates, or throws, the bug is visible only after expansion.
flowchart TB
A["Caller writes macro form"] --> B["Macro receives unevaluated forms"]
B --> C["Macro returns expanded Clojure code"]
C --> D["Expanded code is compiled or evaluated"]
D --> E["Runtime side effects happen here"]
The macro controls where the caller’s forms appear in the generated code. The runtime only sees the expansion.
This macro looks like a harmless logging helper:
1(defmacro bad-log [expr]
2 `(do
3 (println "value:" ~expr)
4 ~expr))
The argument appears twice in the expansion. If the caller passes a side-effecting expression, it runs twice:
1(def counter (atom 0))
2
3(bad-log (swap! counter inc))
4;; prints: value: 1
5;; returns: 2
That result surprises Java engineers because a Java method argument would be evaluated once before method entry. A macro argument is not a value yet.
Bind the expression once in generated code, then use the generated local:
1(defmacro log-once [expr]
2 `(let [value# ~expr]
3 (println "value:" value#)
4 value#))
5
6(log-once (swap! counter inc))
7;; prints: value: 3
8;; returns: 3
The value# symbol is an auto-gensym. Clojure generates a unique local name so the macro’s temporary binding does not collide with caller code.
| Java method call | Clojure macro call |
|---|---|
| Arguments are evaluated before the method body runs. | Arguments are passed as forms to the macro. |
| Reusing a parameter does not rerun the original expression. | Reusing ~expr in the expansion reruns the expression. |
| Runtime debugger sees the method body. | Macro review starts by inspecting the expansion. |
| Check | Safer habit |
|---|---|
| Caller expression appears more than once. | Bind it once with a generated local. |
| Macro controls body execution. | Make skipped, repeated, or delayed execution explicit in the name and docs. |
| Caller form has side effects. | Test with an atom or logging expression to expose repeated evaluation. |
| Expansion is hard to predict. | Use macroexpand-1 before trusting the abstraction. |