Escaping Clojure Exceptions
Given the following code, it’s pretty obvious that we’ve handled the
inevitable exception and we’re going to return :handled, right?
(defn barf [_]
  (throw (Exception. "We've got bugs!")))
(try
  (map barf [:a :b])
  (catch Exception e :handled))
Let’s try it:
1. Unhandled java.lang.Exception
   We've got bugs!
                      REPL:   69  collbox.core/barf
                  core.clj: 2726  clojure.core/map/fn
              LazySeq.java:   40  clojure.lang.LazySeq/sval
              LazySeq.java:   49  clojure.lang.LazySeq/seq
                   RT.java:  525  clojure.lang.RT/seq
                  core.clj:  137  clojure.core/seq
            core_print.clj:   53  clojure.core/print-sequential
            core_print.clj:  160  clojure.core/fn
              MultiFn.java:  233  clojure.lang.MultiFn/invoke
                  main.clj:  243  clojure.main/repl/read-eval-print
...
Wait, what? We took care to catch that exception! Somehow it’s
escaping our try / catch and leaking out.  What’s happening here?
There’s actually a good hint right there in the stacktrace, if you
read carefully: clojure.lang.LazySeq.  The try block is actually
returning a lazy sequence, which means it’s carrying unevaluated code
which can be evaluated at a later time to define a sequence.  This
code isn’t executed until the REPL tries to display the sequence, at
which point it has to force evaluation.  At this point, however, the
code is no longer executing within the context of try. (Props if you
actually saw that coming.)
The solution in this case is quite simple: wrap the expression
returning the lazy sequence in a call to doall to force it to be
fully evaluated (within the try):
(try
  (doall (map barf [:a :b]))
  (catch Exception e :handled))
;; => :handled
Much better.
For experienced Clojurists there’s nothing too surprising here, but
the behavior is a bit counter-intuitive, so it’s good to be reminded
that this can happen.  This definitely can come up in practical
contexts like generating API responses, and it’s less obvious with
multiple layers of function calls between the try and the map.
There’s no silver bullet here, so any time you’re catching exceptions, ask yourself—could this be lazy? If so, force the sequence to be realized or exceptions could escape.