expez
expez

Reputation: 289

How do I get stacktraces from errors in clojure futures?

I have a few tasks that are quite independent that I've spun off using futures. These tasks communicate certain events back to the main app over a core.async/chan, or just talk to the db.

Some of these futures are now failing silently. I get no stacktraces in my logs, or on std{out,err}. I've tried surrounding the code in the fns called by the futures with

(try (do-stuff)
  (catch Exception e
    (log/error e))

just to get some output into my logs, but that--surprisingly!--didn't work.

Is my only option to spin up yet another thread which does the following in a loop?

(let [m (Thread/getAllStackTraces)]
    (doseq [e (.entrySet m)]
      (log/error (.toString (.getKey e)))
      (doseq [s (.getValue e)]
        (log/error " " (.toString s)))))

Is this a symptom indicating that I shouldn't be using futures at all? Should I be using agents, even though there's no need to send any messages to these agents?

Upvotes: 6

Views: 1116

Answers (2)

Domchi
Domchi

Reputation: 10813

This issue is actually tackled by Stuart Sierra here. Go there and read it as it's worth it. In short, his elegant solution is to set default uncaught exception handler:

;; Assuming require [clojure.tools.logging :as log]
(Thread/setDefaultUncaughtExceptionHandler
  (reify Thread$UncaughtExceptionHandler
    (uncaughtException [_ thread ex]
      (log/error ex "Uncaught exception on" (.getName thread)))))

Upvotes: 0

Konrad Garus
Konrad Garus

Reputation: 54035

The behavior is very similar to Java Future. Inside the future block exceptions may be thrown and caught, and that behaves as you would expect. When an exception is not caught, the Future has no way of rethrowing it on the calling thread. It only does so in form of ExecutionException when you actually get its value. This corresponds to deref in Clojure.

Let's create a function that throws something:

(defn die [] (throw (RuntimeException.)))

If I just wrap it in future, it works fine:

user=> (def x (future (die)))
#'user/x
; Note: No exception here
user=> @x
RuntimeException   user/die (NO_SOURCE_FILE:1)
; Bam! Exception thrown on deref, think of it as
; ExecutionException when getting failed future's value in Java

So you can catch this exception on deref:

user=> (def x (future (die)))
#'user/x
(try @x (catch Exception e (println "Caught ya")))
Caught ya
nil

Or you can catch it inside the future:

user=> (def x 
  #_=>   (future 
  #_=>     (try (die)
  #_=>       (catch Exception e
  #_=>         (print "Caught ya!")
  #_=>         "Something"))))
#'user/x
Caught ya
user=> @x
"Something"

Note how in this case it prints "Caught ya" immediately when the error occurs on the background thread, before deref. Then on deref it returns value returned by the catch block in future.

Once again, the bottom line is - it works pretty much the same as Java futures.

Upvotes: 8

Related Questions