Browse Learn Clojure Foundations as a Java Developer

Improve Error Reporting with Clojure Macros

Learn how Clojure macros can preserve source expressions for diagnostics, when a function is enough, and how to integrate macro-generated context with ordinary JVM exception handling.

Error-reporting macros are useful when the diagnostic needs the caller’s source form, not just its runtime value. Java logging code usually sees values and stack traces. A Clojure macro can also preserve the expression the caller wrote.

Use this power sparingly. Most logging, exception wrapping, and telemetry should remain ordinary functions or libraries.

Capture the Source Form

 1(defmacro with-context [context & body]
 2  (let [form (list 'quote body)]
 3    `(try
 4       ~@body
 5       (catch Exception e#
 6         (throw
 7           (ex-info "Expression failed"
 8                    {:context ~context
 9                     :form ~form}
10                    e#))))))

Usage:

1(with-context {:job-id job-id}
2  (import-users! db file))

If the body throws, the exception data includes both runtime context and the quoted body form. That helps when a failure report needs to identify the expression that failed.

What a Function Cannot See

A function can receive the result of an expression, or it can receive an explicit thunk:

1(with-context-fn {:job-id job-id}
2  (fn []
3    (import-users! db file)))

That is sometimes enough. The macro earns its place only when preserving the original source form improves the diagnostic enough to justify the extra complexity.

Java Comparison

Java habit Clojure macro option
Log exception message and stack trace. Keep doing that with JVM logging tools.
Add contextual fields such as request ID. Use functions or middleware when possible.
Capture the exact expression being evaluated. A macro can quote the caller form.
Wrap every operation automatically. Be careful: broad macros can hide control flow.

Keep Logging Libraries Outside the Macro

The macro can create context; a function should do the actual reporting:

 1(defn report-exception [data exception]
 2  ;; Connect this to your logging or telemetry library.
 3  (println "failure:" data (.getMessage exception)))
 4
 5(defmacro reporting [data & body]
 6  (let [form (list 'quote body)]
 7    `(try
 8       ~@body
 9       (catch Exception e#
10         (report-exception (assoc ~data :form ~form) e#)
11         (throw e#)))))

This split keeps Java interop with logging frameworks in normal functions, where it is easier to test and replace.

Diagnostic Macro Checklist

Question Good answer
Does the diagnostic need the unevaluated source form? Yes, otherwise use a function.
Does the macro rethrow the original exception? Yes, with useful context attached or logged.
Is logging implementation kept outside the macro? Yes, ordinary functions own runtime work.
Could the macro accidentally hide control flow? No, the wrapper behavior is explicit.

Knowledge Check

### What can an error-reporting macro capture that a normal function cannot receive directly? - [x] The caller's unevaluated source form. - [ ] The JVM bytecode verifier. - [ ] Every object allocated by the program. - [ ] Java checked exception declarations. > **Explanation:** A macro receives forms before evaluation, so it can quote the expression the caller wrote and include it in diagnostics. ### When is a function enough for error reporting? - [x] When runtime values, context maps, and normal exception data provide enough information. - [ ] When you need the exact source form automatically. - [ ] When the body must remain unwrapped. - [ ] When exceptions should be swallowed. > **Explanation:** Most reporting works with runtime values. A macro is only justified when source-form context materially improves the diagnostic. ### Why keep the logging library call in a helper function? - [x] Runtime integration is easier to test and replace outside the macro. - [ ] Macros cannot call functions. - [ ] Clojure cannot interoperate with Java logging. - [ ] Helper functions prevent all exceptions. > **Explanation:** Macros should stay small. Ordinary functions are better for runtime logging, Java interop, and testable behavior.
Revised on Saturday, May 23, 2026