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.