Error Handling in Clojure

Use exceptions, ex-info, explicit error values, and validation boundaries deliberately so failures stay diagnosable without becoming normal control flow.

Error handling in Clojure works best when you separate expected failures from unexpected failures. Validation errors, missing business data, or permission denials are usually part of the program’s ordinary outcomes and should often be represented as data. Broken I/O, violated invariants, or corrupted assumptions usually deserve exceptions.

Choose the Failure Shape Before You Choose the Syntax

The most important question is not “Should I use try?” but “What kind of failure is this?”

  • Expected domain failure: return a value that callers can handle explicitly.
  • Boundary or infrastructure failure: catch, enrich, and translate exceptions near the edge.
  • Programmer error or broken invariant: fail loudly and fix the code.

That distinction keeps your code from turning every missing field into a thrown exception while also preventing silent corruption.

    flowchart TD
	    A["Failure happens"] --> B{"Expected part of domain flow?"}
	    B -->|Yes| C["Return error as data"]
	    B -->|No| D{"Boundary or I/O problem?"}
	    D -->|Yes| E["Throw or catch exception with context"]
	    D -->|No| F["Fail fast on broken invariant"]

Use Exceptions for Exceptional Conditions

Clojure inherits JVM exceptions, and that is appropriate for operations that can fail because the outside world is unreliable.

 1(defn read-config! [path]
 2  (try
 3    (slurp path)
 4    (catch java.io.FileNotFoundException e
 5      (throw (ex-info "Configuration file not found"
 6                      {:path path
 7                       :kind :config/missing}
 8                      e)))
 9    (catch Exception e
10      (throw (ex-info "Unable to load configuration"
11                      {:path path
12                       :kind :config/unreadable}
13                      e)))))

ex-info is usually the most useful way to throw application-level exceptions in Clojure because it adds structured context through ex-data.

1(try
2  (read-config! "config.edn")
3  (catch clojure.lang.ExceptionInfo e
4    (println "kind:" (:kind (ex-data e)))
5    (println "path:" (:path (ex-data e)))))

Good exception practice in Clojure:

  • catch exceptions at system boundaries, not everywhere
  • enrich them with ex-info and ex-data
  • log once at the layer that can add operational context
  • do not use exceptions for common business branching

Return Data for Expected Business Outcomes

If the caller is expected to choose a different branch based on failure, returning data is often clearer than throwing.

 1(defn create-user [{:keys [email]}]
 2  (cond
 3    (nil? email)
 4    {:status :invalid
 5     :errors [:email-required]}
 6
 7    (re-find #"@" email)
 8    {:status :ok
 9     :user {:id (random-uuid)
10            :email email}}
11
12    :else
13    {:status :invalid
14     :errors [:email-format-invalid]}))

This style works well for service handlers, command processing, and validation-heavy workflows because the caller can decide how to render the error without first unpacking a thrown exception.

1(case (:status (create-user {:email "bad"}))
2  :ok  {:http-status 201}
3  :invalid {:http-status 400
4            :body {:errors [:email-format-invalid]}})

You do not need a full Either or Result library to get the benefit. Ordinary maps are often enough, especially when your team already has a consistent shape for success and error values.

Assertions, Preconditions, and Validation Are Different Tools

:pre and :post conditions are useful for internal contracts, but they are not a substitute for user-input validation.

1(defn average
2  [values]
3  {:pre [(seq values)
4         (every? number? values)]}
5  (/ (reduce + values) (count values)))

Use these when violating the contract means the program is being used incorrectly. Do not rely on them as your primary runtime validation layer for web forms, API payloads, or untrusted external input.

For boundary validation, explicit validation functions or libraries such as Malli or clojure.spec.alpha are usually better because they produce explainable error data.

A Useful Layering Pattern

A common production pattern is:

  1. validate inputs and return structured error data
  2. run domain logic as pure functions where possible
  3. catch infrastructure exceptions at the edge
  4. translate exceptions into HTTP, CLI, or job-level outcomes
 1(defn save-order! [datasource order]
 2  (try
 3    ;; write to database here
 4    {:status :ok :order order}
 5    (catch Exception e
 6      (throw (ex-info "Failed to persist order"
 7                      {:order-id (:id order)
 8                       :kind :db/write-failed}
 9                      e)))))
10
11(defn handle-create-order [datasource command]
12  (let [validated (if (seq (:items command))
13                    {:status :ok :command command}
14                    {:status :invalid :errors [:empty-items]})]
15    (if (= :invalid (:status validated))
16      validated
17      (save-order! datasource (:command validated)))))

That keeps validation, domain rules, and I/O failures from being mixed into one indistinct “error handling” blob.

When Functional Error Libraries Help

Libraries inspired by Either or Result types can help when you need:

  • heavy composition across many failure-returning steps
  • a consistent pipeline abstraction across a large codebase
  • explicit left/right semantics for domain operations

They are optional, not mandatory. For many Clojure teams, plain maps plus disciplined conventions are simpler and more idiomatic than importing category-theory vocabulary into otherwise straightforward application code.

Common Mistakes

  • throwing exceptions for expected validation failures
  • swallowing exceptions and returning vague strings
  • catching Exception everywhere without adding context
  • using :pre/:post as if they were user-facing validation
  • logging the same failure repeatedly at every layer

The goal is not “no exceptions.” The goal is that failures carry the right amount of structure and appear at the right architectural boundary.

Ready to Test Your Knowledge?

Loading quiz…
Revised on Thursday, April 23, 2026