Some more do’s and don’ts for you. This time it’s a do.’
In the JVM, when an exception is thrown on a thread other than the main thread, and nothing is there to catch it, nothing happens. The thread dies silently.
This is bad news if you needed that thread to do some work. If all the worker threads die, the application could appear to be “up” but cease to do any useful work. And you’ll never know why.
In Clojure, this could happen on any thread you created with
core.async/thread, a worker thread used by
core.async/go, or a thread that was created for you by a Java framework such as a Servlet container.
One solution is to just wrap the body of every
go in a try/catch block. There are good reasons for doing this: you can get fine-grained control over how exceptions are handled. But it’s easy to forget, and it’s tedious to repeat if you can’t do anything useful with the exception besides log it.
So at a minimum, I recommend always including this snippet of code somewhere in the start-up procedure of your application:
;; Assuming require [clojure.tools.logging :as log] (Thread/setDefaultUncaughtExceptionHandler (reify Thread$UncaughtExceptionHandler (uncaughtException [_ thread ex] (log/error ex "Uncaught exception on" (.getName thread)))))
This bit of code has saved my bacon more times than I can count.
This is a global, JVM-wide setting. There can be only one default uncaught exception handler. Individual Threads and ThreadGroups can have their own handlers, which get called in preference to the default handler. See Thread.setDefaultUncaughtExceptionHandler.
I’ve tried more aggressive measures, such as terminating the whole JVM process on any uncaught exception. While I think this is technically the correct thing to do, it turns out to be annoying in development.
Also annoying is the fact that some Java frameworks are designed to let threads fail silently. They just allocate a new thread in a pool and keep going. If your application is logging lots of uncaught exceptions but appears to be working normally, look to your container framework to see if that’s expected behavior.
The Hidden Future
Another wrinkle: exceptions inside a
future are always caught by the Future. The exception will not be thrown until something calls Future.get (
deref in Clojure).
Be aware that ExecutorService.submit returns a Future, so if you’re using an ExecutorService you need to make sure something is eventually going to consume that Future to surface any exceptions it might have caught.
The parent interface Executor.execute does not return a Future, so exeptions will reach the default exception handler.
Using ExecutorService.submit instead of Executor.execute was a bug in very early versions of core.async.