Browse Learn Clojure Foundations as a Java Developer

Create Custom Control Flow with Clojure Macros

Learn when a Clojure macro is justified for custom control flow, how to wrap a body without repeated evaluation, and why Java-style loop imitation is usually the wrong goal.

Custom control flow is the classic macro use case. The macro receives a body as unevaluated forms, decides where that body goes, and returns ordinary Clojure code.

That does not mean every missing Java keyword deserves a macro. Clojure already has if, when, cond, case, loop, recur, doseq, sequence operations, and higher-order functions. A custom control macro should make evaluation clearer, not recreate Java syntax out of habit.

A Useful Wrapper: Timing a Body

This macro keeps the caller’s body as normal Clojure code while wrapping it in timing behavior:

1(defmacro with-timing [label & body]
2  `(let [started# (System/nanoTime)]
3     (try
4       ~@body
5       (finally
6         (println ~label "took"
7                  (/ (- (System/nanoTime) started#) 1000000.0)
8                  "ms")))))

Usage stays readable:

1(with-timing "load users"
2  (load-users db)
3  (refresh-cache!))

A function can receive a thunk, but the call site becomes less natural:

1(with-timing-fn "load users"
2  (fn []
3    (load-users db)
4    (refresh-cache!)))

The macro is justified only if preserving the direct body shape matters enough to accept macro complexity.

Function or Macro?

Need Prefer
Transform an already computed value. Function.
Pass behavior explicitly as a value. Function taking a function.
Control whether, when, or how often body forms run. Macro.
Hide ordinary code behind clever syntax. Do not do it.

Review the Expansion

1(macroexpand-1
2  '(with-timing "load users"
3     (load-users db)))

When reviewing the expansion, check that the body appears exactly where intended, temporary locals are generated, and no caller expression is duplicated.

Avoid Java Syntax Nostalgia

Java developers often reach for a macro to recreate a familiar loop:

1(repeat-until done?
2  (step!))

That can be valid, but it is often less clear than an ordinary Clojure loop or sequence pipeline. Before adding a control macro, ask whether a reader would understand the expansion faster than they would understand the macro name.

Control-Flow Macro Checklist

Question Good answer
Does the macro need unevaluated body forms? Yes, otherwise a function is simpler.
Does the body run zero, one, or many times? The name and docs make that obvious.
Are temporary locals generated? Yes, with # or gensym.
Can a teammate inspect the expansion quickly? Yes, the generated code is small.

Knowledge Check

### When is a control-flow macro justified over a function? - [x] When it must control evaluation of unevaluated body forms. - [ ] Whenever it makes code look more like Java. - [ ] Whenever it avoids one helper function. - [ ] When all arguments are already computed values. > **Explanation:** Macros are justified when syntax and evaluation control are central. Ordinary value transformation belongs in functions. ### What should you check in the expansion of `with-timing`? - [x] The body appears once, temporary locals are generated, and the wrapper is clear. - [ ] The macro expands into Java source. - [ ] The body is evaluated before macro expansion. - [ ] The expansion hides every implementation detail. > **Explanation:** Macro review is generated-code review. The expansion should make evaluation order and generated names clear. ### Why might a function with a thunk be less ergonomic than a macro here? - [x] The caller must wrap the body in an explicit `fn`. - [ ] Functions cannot run on the JVM. - [ ] Functions always evaluate their bodies twice. - [ ] Functions cannot call Java methods. > **Explanation:** A macro can preserve a natural body shape. A function needs behavior passed as a value, usually with `fn`.
Revised on Saturday, May 23, 2026