Understand what monadic error handling means conceptually, why Clojure often favors data-first result values, and when Maybe- or Either-like patterns improve composition.
Monad: A way to sequence computations that carry extra context, such as possible failure, optional values, or asynchronous execution, without manually unpacking that context at every step.
Monads are a useful concept for understanding functional error handling, but they are not the everyday center of most idiomatic Clojure code. In practice, Clojure teams often use a data-first style that looks monadic in spirit even when it is not packaged as a formal monad library.
That distinction matters. If you approach this topic as “Clojure needs Haskell-style monads everywhere,” the page becomes misleading. If you approach it as “how do I compose computations that may fail without turning everything into nested conditionals and ad hoc exceptions?”, the topic becomes practical.
Ordinary exception-driven code can make composition awkward when failure is part of the expected control path.
For example:
1(defn parse-port [s]
2 (Integer/parseInt s))
3
4(defn valid-port? [n]
5 (<= 1 n 65535))
If parsing fails by throwing, and validation is a separate step, the composition story lives partly in return values and partly in exception flow. That can be fine for truly exceptional failures, but it is often awkward for expected, user-driven invalid input.
Functional error handling tries to make failure part of the value flow.
Two of the most common conceptual models are:
These are useful even if you never adopt a specific monad library.
Maybe-style handling is appropriate when “missing result” is enough information.
1(defn safe-reciprocal [n]
2 (when (not (zero? n))
3 (/ 1.0 n)))
4
5(some-> 4
6 safe-reciprocal
7 Math/sqrt)
8;; => 0.5
9
10(some-> 0
11 safe-reciprocal
12 Math/sqrt)
13;; => nil
This is not a formal monad tutorial example, but it captures the same idea: once the value disappears, the rest of the chain does not continue meaningfully.
Either-style handling is better when the caller needs to know why the computation failed.
1(defn parse-port [s]
2 (let [n (parse-long s)]
3 (cond
4 (nil? n)
5 {:ok? false
6 :error :not-a-number}
7
8 (not (<= 1 n 65535))
9 {:ok? false
10 :error :out-of-range}
11
12 :else
13 {:ok? true
14 :value n})))
That result is not formally branded as Either, but it behaves similarly:
This style is extremely common in Clojure because plain maps are already a strong medium for domain results.
Clojure tends to prefer simple data and explicit composition over heavy typeclass-style abstraction. That means many teams reach first for:
nil-aware flows such as some->{:ok? true :value ...} or {:ok? false :error ...}ex-info when failure is exceptional and should unwind normallyThe advantage is readability. You do not need a large abstraction layer to express the success/failure contract. The trade-off is that you lose some of the uniform interface that formal monadic APIs can provide.
There are situations where a proper monadic API is useful:
But the modern Clojure lesson should be precise here: monads are a conceptual tool first, not a mandatory default abstraction for all error handling.
The value of monadic thinking is easiest to see when several steps can fail.
1(defn parse-port [s]
2 (let [n (parse-long s)]
3 (cond
4 (nil? n) {:ok? false :error :not-a-number}
5 :else {:ok? true :value n})))
6
7(defn validate-port [n]
8 (if (<= 1 n 65535)
9 {:ok? true :value n}
10 {:ok? false :error :out-of-range}))
11
12(defn reserve-port [n]
13 {:ok? true :value {:port n :status :reserved}})
Without a composition helper, callers may end up writing nested if chains. The functional goal is to keep the error path explicit while avoiding deeply branching glue code.
One direct way is a small binder helper:
1(defn bind-result [result f]
2 (if (:ok? result)
3 (f (:value result))
4 result))
5
6(-> (parse-port "8080")
7 (bind-result validate-port)
8 (bind-result reserve-port))
9;; => {:ok? true, :value {:port 8080, :status :reserved}}
This is monadic in spirit: if the current step succeeded, continue with the next function; otherwise propagate the failure unchanged.
This page is not arguing that exceptions are always wrong.
Exceptions are still appropriate when:
The design question is not “exceptions or monads forever.” It is:
Use the lightest error model that preserves clarity.
nil or some-> when “missing value” is enoughThat rule will help most Clojure code more than forcing formal monadic terminology into every module.
For Clojure, “functional error handling with monads” is best understood as:
Sometimes that representation is a dedicated library. Often it is ordinary Clojure data and a few small helper functions.
A team validates request input with nested try/catch blocks and booleans. They want a style that composes several validation steps cleanly and preserves useful error information, but they do not want to drag the whole codebase into heavy abstraction jargon.
What is the stronger direction?
The stronger direction is usually to model success and failure as explicit data, then add small composition helpers where needed. That captures the core value of monadic error handling without forcing the team into a library-first design unless the extra abstraction is clearly paying for itself.
nil, result maps, or exceptions according to the needs of the failure model