Elixir is dynamic and has a pretty different approach to this, clojure can learn about it
We’ve discussed this before here Error handling in clojure
In that thread, we weren’t able to actually identify what problems Elixir solved in its handling of exception over Clojure’s. Thus I still stand by the fact they’re both just as good and it comes down to personal preference and familiarity.
I welcome reviving the conversation if you have new datapoints of example cases that are problematic to handle in Clojure, but not in Elixir.
I would guess that the foremost reason clojure is fine with exceptions is that it’s hosted on runtimes that all use exceptions fairly liberally. My feeling is that exceptions tend to be slightly over-used but there’s nothing fundamentally bad about them - it’s useful to have a “bail out” mechanism that works its way up the callstack by default.
The one thing I haven’t seen mentioned in this thread is the Common Lisp extension/alternative of the condition/restart system. It’s a mechanism to allow code up the call stack to decide on what to do in an “exceptional” condition, which gets to pick from a number of strategies provided by the callee. When a condition like that works its way up to the REPL the user/programmer can even pick a strategy at that point and the program can just continue. Pretty magical when you run into it first.
There are a few implementations of the mechanism in the Clojure ecosystem but none have caught on. I’m not really sure why not.
There is an abstraction that is less powerful than a Monad but which already makes your life better: Functors.
Imagine you want to write a function that adds 3 to its argument. Easy.
Now however assume that the argument could be nil in which case you just want to reply with nil.
(defun foo1 (n) (when-not (nil? n) (+ n 3)))
And now let’s say that you want to write a similar function where you have a list of numbers and want to add 3 to each of them:
(defun foo2 (nums) (map #(+ % 3) nums))
Or what if you want to write a function to which you pass another function which either returns a number or which throws an exception?
(defun foo3 (f) (try (+ (f) 3) (catch e ...)))
Or you have a hashmap in which all the values are numbers and you want to add 3 to each of them and thus generate a new map:
(defun foo4 (hm) (zipmap (keys hm) (map #(+ % 3) (vals hm))))
In Haskell you would be write an implementation against Functors. In Clojure they exist too, as well as monads. So in Haskell you would express it like this:
foo = fmap (+3)
And in Clojure you could maybe say:
(def foo (partial fmap #(+ % 3)))
And that’s it.
This foo can potentially replace all of the foo versions above. So it is much more reusable. Composability is the holy grail of functional programming and monads compose very well.
We should call out Scott Wlaschin’s amazing series of blog posts and presentations on this topic (amongst many!) which he terms Railway Oriented Programming (ROP). His focus is F# and typed functional languages, and so a little purist compared to what’s required in Clojure. But the principles of the benefits of composition etc are really nicely illustrated. https://fsharpforfunandprofit.com/posts/recipe-part2/
In our team we’ve used Adam Bard’s err->> macro verbatim for a long time. More recently a team member - inspired by Scott’s articles - wrote a Clojure version of ROP. This started off following Scott’s patterns slavishly but then evolved to be more idomatic Clojure. We use both in our code now. https://github.com/HughPowell/railway-oriented-programming
Both show how Clojure can be adaptable to these patterns in a few lines of code without explicitly needing much of the functional apparatus like “map” and “lift” that Scott Wlaschin so nicely defines in his (algebraically) typed FP landscape.
Completely agree with other posters that adoption of this pattern is a choice - there should be no dogma about “good” or “bad” Clojure or FP. However I found this area to be one of those ways into the FP world that I enjoyed and which has brought benefits to our team - i.e. the ease of composition and reduction of magic code paths that comes about with exception habdling.
Note one titbit of advice might be to not catch every exception. If something is vaguely recoverable catch and pass the error down the (monadic) chain - but throw exceptions for truly unexpected and unrecoverable cases. Again probably too pragmatic for some!