Why Are My (Clojure) Stack Traces Missing? The Little-Known OmitStackTraceInFastThrow Flag
ProblemLink to 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]
SolutionLink to 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:
- Pass the option to java when running a jar file:
java -XX:-OmitStackTraceInFastThrow myjar.jar
- Set the
JAVA_TOOL_OPTIONS
env varJAVA_TOOL_OPTIONS=-XX:-OmitStackTraceInFastThrow
- (Clojure-specific) Set
:jvm-opts ["-XX:-OmitStackTraceInFastThrow"]
in yourdeps.edn
orproject.clj
- (Clojure-specific) Use
clj -J-XX:-OmitStackTraceInFastThrow
with Clojure CLI
DiscussionLink to 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 ProblemLink to 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)