Why Are My (Clojure) Stack Traces Missing? The Little-Known OmitStackTraceInFastThrow Flag
Why Are My (Clojure) Stack Traces Missing? The Little-Known OmitStackTraceInFastThrow Flag

Problem

Sometimes you see exceptions on the JVM that are missing the message and the stack trace. They can look like this in your logs:

ERROR java.lang.ClassCastException

Or perhaps like this if you're using Clojure (printed via prn):

#error {
 :cause nil
 :via
 [{:type java.lang.ClassCastException
   :message nil}]
 :trace
 []}

Or perhaps like this (printed via clojure.stacktrace/print-stack-trace):

java.lang.ClassCastException: null
 at [empty stack trace]

Solution

This happens because of a HotSpot optimization. The release notes for JDK 5.0 tell us:

After recompilation, the compiler may choose a faster tactic using preallocated exceptions that do not provide a stack trace. To disable completely the use of preallocated exceptions, use this new flag: -XX:-OmitStackTraceInFastThrow.

This little-known flag has been around since Java 5! So to get stack traces for your exceptions, you need to set the -XX:-OmitStackTraceInFastThrow JVM flag. Note that this flag disables the OmitStackTraceInFastThrow feature due to the - before Omit.

Here are some ways you can set the flag:

  1. Pass the option to java when running a jar file: java -XX:-OmitStackTraceInFastThrow myjar.jar
  2. Set the JAVA_TOOL_OPTIONS env var JAVA_TOOL_OPTIONS=-XX:-OmitStackTraceInFastThrow
  3. (Clojure-specific) Set :jvm-opts ["-XX:-OmitStackTraceInFastThrow"] in your deps.edn or project.clj
  4. (Clojure-specific) Use clj -J-XX:-OmitStackTraceInFastThrow with Clojure CLI

Discussion

Why is omitting stack traces the default? Some Java code uses exceptions for control flow, and can end up throwing lots of exceptions in a hotspot. Code that uses exceptions for control flow usually only cares about the class of the exception, not the message or the stack trace. Leaving out the stack trace can make throwing the exception orders of magnitude faster so HotSpot helpfully performs this optimization for you.

I'd argue that since using exceptions for control flow is a bad idea anyway, you don't want to benefit from this optimization. Also, in my experience making production problems harder to debug (by throwing away important error messages) is not worth a slight performance increase. I recommend starting every project with -XX:-OmitStackTraceInFastThrow, measuring performance, and optimizing hotspots where necessary. If it turns out you need to turn on OmitStackTraceInFastThrow, do it consciously after evaluating the pros and cons for your project.

For fun, I ran a quick poll on the Clojurians Slack to check how many people know this flag. Here are the results. Turns out that at least among this Slack crowd the flag is fairly well-known.

PS. Reproducing The Problem

I had to try out a couple of ways to reproduce the problem. The following snippet seems the most reliable so far. The exception needs to get thrown multiple times for HotSpot to optimize the throw.

(dotimes [i 10000]
  (try (dissoc (list) :x)
       (catch Exception e nil)))

(dissoc (list) :x)