Error Handling
Prerequisites: Functions, Maps
Clojure uses the JVM's exceptions, with try, catch, and finally,
plus a convenient way to attach data to errors.
(defn parse-age [s]
(let [n (parse-long (str s))]
(cond
(nil? n) (throw (IllegalArgumentException. (str "not a number: " (pr-str s))))
(neg? n) (throw (IllegalArgumentException. "age cannot be negative"))
:else n)))
(doseq [input ["42" "abc" "-5"]]
(try
(println "age =" (parse-age input))
(catch IllegalArgumentException e
(println "error:" (ex-message e)))
(finally
(println " (checked)"))))
age = 42
(checked)
error: not a number: "abc"
(checked)
error: age cannot be negative
(checked)
(throw exception)raises;(ClassName. args)constructs a Java object.catchclauses name an exception class; the first matching one runs.finallyalways runs — the place for cleanup.- Clojure has no checked exceptions, like Groovy and Kotlin.
ex-info: errors with data
(defn withdraw [account amount]
(if (> amount (:balance account))
(throw (ex-info "Insufficient funds"
{:type :insufficient-funds
:balance (:balance account)
:requested amount}))
(update account :balance - amount)))
(println (withdraw {:id 1 :balance 100} 30))
(try
(withdraw {:id 1 :balance 100} 500)
(catch clojure.lang.ExceptionInfo e
(let [{:keys [type balance requested]} (ex-data e)]
(println (ex-message e) type balance requested))))
{:id 1, :balance 70}
Insufficient funds :insufficient-funds 100 500
ex-info creates an exception carrying a map of data; ex-data
reads it back. This is the idiomatic way to signal domain errors — no
custom exception classes needed. Callers can decide what to do based on
:type.
Catching broadly and wrapping
(defn load-config [text]
(try
(let [n (Long/parseLong text)]
{:workers n})
(catch NumberFormatException e
(throw (ex-info "Bad config" {:input text} e)))))
(try
(load-config "many")
(catch Exception e
(println (ex-message e) (ex-data e))
(println "caused by:" (type (ex-cause e)))))
Bad config {:input many}
caused by: java.lang.NumberFormatException
The third argument of ex-info is the cause, so the original
exception isn't lost. (catch Exception e ...) catches everything
except JVM Errors.
Errors as values
(defn safe-divide [a b]
(if (zero? b)
{:error :division-by-zero}
{:ok (/ a b)}))
(doseq [[a b] [[10 4] [1 0]]]
(let [{:keys [ok error]} (safe-divide a b)]
(println (if error (str "failed: " (name error)) (str "result: " ok)))))
result: 5/2
failed: division-by-zero
For expected failures (validation, "not found"), returning a map is often clearer than throwing. Save exceptions for truly exceptional situations.
Practice
Write fetch-user that throws ex-info with {:type :not-found :id id}
when the id isn't in a map of users, and a caller that prints
"user 7 not found".
Show solution
(def users {1 "Ada" 2 "Linus"})
(defn fetch-user [id]
(or (get users id)
(throw (ex-info "User not found" {:type :not-found :id id}))))
(println (fetch-user 1))
(try
(fetch-user 7)
(catch clojure.lang.ExceptionInfo e
(when (= :not-found (:type (ex-data e)))
(println "user" (:id (ex-data e)) "not found"))))
Ada
user 7 not found