Common Clojure error-handling failures, including swallowed exceptions, vague ex-info usage, and broken propagation boundaries.
Poor error handling in Clojure rarely means “using exceptions at all.” It usually means losing the information, timing, or boundary clarity that makes failures diagnosable. The code catches too much, swallows too much, logs the same failure three times, or throws generic exceptions with no useful context.
A good error-handling design answers a few practical questions:
When those answers are muddy, the code becomes much harder to operate.
This is the easiest error-handling mistake to write and one of the hardest to diagnose later.
1(try
2 (do-something-risky)
3 (catch Exception _
4 nil))
Now the system has lost:
Sometimes returning nil is legitimate, but it should represent an actual domain absence, not a hidden exception.
The anti-pattern is turning a concrete failure into silent ambiguity.
Exception Too EarlyCatching broad exceptions at low levels often hides the real semantics of failure.
Weak:
1(defn load-config [path]
2 (try
3 (slurp path)
4 (catch Exception e
5 (throw (Exception. "config failed")))))
This strips away useful details and replaces them with a message that is both generic and harder to debug.
If you wrap an exception, preserve context:
1(defn load-config [path]
2 (try
3 (slurp path)
4 (catch java.io.IOException e
5 (throw (ex-info "Failed to load config"
6 {:path path
7 :type :config/io-failure}
8 e)))))
Now the caller gets:
The anti-pattern is catching broadly without improving the failure model.
Duplicate logging is a common operational problem.
Example:
This creates noisy logs without adding real information.
A better rule is usually:
The anti-pattern is confusing “adding context” with “logging again.”
Plain exception messages are often not enough in a data-oriented language like Clojure.
Weak:
1(throw (Exception. "Invalid order"))
Stronger:
1(throw
2 (ex-info "Order failed validation"
3 {:type :order/invalid
4 :order-id (:id order)
5 :missing-fields missing-fields}))
Structured data matters because callers and logs can inspect it without brittle message parsing.
The anti-pattern is flattening a rich failure into an unstructured string too early.
Not every negative path is exceptional.
Expected cases often deserve ordinary data:
Sometimes a result map, tagged value, or explicit domain status is more honest than throwing.
The anti-pattern is using exceptions for control flow when the outcome is a normal business branch rather than an operational failure.
Error handling becomes messy when every layer invents its own policy.
Questions to answer explicitly:
If those boundaries are unclear, code ends up with:
ex-infoWrapping an error without preserving the original cause is another expensive mistake.
If you throw a new exception and discard the old one, diagnosis gets much harder. In Clojure, ex-info with a cause keeps the chain intact and is usually the better default for enriched exception context.
The anti-pattern is replacing a useful failure lineage with a prettier but less informative top-level message.
flowchart TD
A["Low-level failure"] --> B["Add structured context at boundary"]
B --> C{"Recoverable domain case?"}
C -->|Yes| D["Return explicit data result"]
C -->|No| E["Propagate exception with cause"]
E --> F["Log once at operational boundary"]
F --> G["Translate for user or caller"]
This model keeps context, avoids duplicate noise, and makes recovery decisions explicit.
ex-info when adding domain-specific failure contextnil or falsey ambiguityException without improving the failure model