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.
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.
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 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. |
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.
| 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. |