Browse Learn Clojure Foundations as a Java Developer

Handle Errors in Clojure Macros

Learn how Clojure macros should validate call syntax, throw useful expansion-time exceptions, and keep generated runtime errors close to the caller's mistake.

Macro errors are harder to diagnose when they point at generated code instead of the bad call site. A production macro should fail early when the call syntax is invalid and generate ordinary runtime errors when the generated code fails.

The first distinction is important:

Error kind Happens when Good response
Invalid macro syntax During expansion Throw ex-info with the bad form or arguments.
Generated runtime failure When expanded code runs Keep generated code small and stack traces readable.
Internal macro bug During expansion or runtime Add expansion tests and shrink the macro.

Validate the Call Shape

Do not let a malformed macro call produce a confusing expansion.

 1(defmacro with-required
 2  [binding & body]
 3  (when-not (and (vector? binding)
 4                 (= 2 (count binding)))
 5    (throw (ex-info "with-required expects [name expr]"
 6                    {:binding binding})))
 7  (let [[name expr] binding]
 8    `(let [value# ~expr]
 9       (when (nil? value#)
10         (throw (ex-info "Required value was nil"
11                         {:name '~name})))
12       (let [~name value#]
13         ~@body))))

This macro checks its call syntax before generating code. If the caller writes (with-required user ...), the error explains the expected shape.

Prefer ex-info for Macro Misuse

ex-info carries structured data, which is more useful than a string alone.

Data key Why include it
:binding Shows the malformed input shape.
:form Useful when the macro receives the whole form.
:expected Documents the contract in machine-readable data.
:macro Helps when multiple macros share validation helpers.

For Java engineers, think of this like throwing IllegalArgumentException with fields you can inspect, not only a message.

Avoid assert for Public Macro Contracts

assert can be disabled depending on runtime settings and is usually too blunt for public macro contracts. Use explicit checks and throw for call-shape validation.

1(when-not (symbol? name)
2  (throw (ex-info "Expected a symbol name"
3                  {:macro 'defhandler
4                   :name name})))

Use assertions for internal invariants if your team already treats them that way, but do not rely on them as user-facing macro validation.

Keep Runtime Errors Ordinary

Generated code should fail like ordinary Clojure where possible.

1(with-required [user (find-user id)]
2  (:email user))

If find-user returns nil, the generated code throws a clear ex-info naming the missing binding. If (:email user) fails later because of a domain problem, that runtime failure should remain visible as normal application behavior.

Macro Error Checklist

Check Good answer
Does the macro validate call shape? Yes, before generating code.
Does the exception name the macro contract? Yes, message plus ex-info data.
Does generated code stay small? Yes, stack traces are not buried in a giant expansion.
Are error cases tested? Yes, invalid syntax and representative runtime failures.

Knowledge Check

### Why should a macro validate its call shape early? - [x] To fail near the bad call with a useful message and data. - [ ] To make generated code run faster. - [ ] To avoid all runtime errors. - [ ] To turn the macro into a function. > **Explanation:** Invalid macro syntax is easiest to fix when the expansion-time exception explains what shape was expected. ### Why prefer `ex-info` for public macro misuse? - [x] It provides a message plus structured diagnostic data. - [ ] It automatically retries macro expansion. - [ ] It disables Java exceptions. - [ ] It prevents compilation. > **Explanation:** Structured data such as `:binding`, `:expected`, or `:macro` helps the caller and tests identify the exact problem. ### Why avoid `assert` for public macro contracts? - [x] Assertions can be disabled and do not give as much control over user-facing errors. - [ ] Assertions cannot appear in Clojure code. - [ ] Assertions always run twice. - [ ] Assertions prevent macro expansion. > **Explanation:** Explicit validation with `throw` is clearer for macro APIs that other developers will call.
Revised on Saturday, May 23, 2026