On This Page
throw new RuntimeException(e.getMessage());
That one line is the reason a Caused by goes missing, and I've seen it in more codebases than I'd like to admit. The message survives. Every frame underneath it, the part that says where things went wrong, is gone. We'll break it on purpose below, but first the healthy version of the problem: a trace with three exceptions stacked in it.
Three exceptions, one root cause, and ... 2 more
This is from a small config loader, OpenJDK 21.0.12.1 on Linux, compiled with plain javac and run as java -cp out Lab chain. Copied from the console as-is:
Exception in thread "main" java.lang.IllegalStateException: Lab$ConfigException: cannot load /etc/app/app.properties
at Lab.loadWrapped(Lab.java:40)
at Lab.main(Lab.java:56)
Caused by: Lab$ConfigException: cannot load /etc/app/app.properties
at Lab.load(Lab.java:24)
at Lab.loadWrapped(Lab.java:38)
... 1 more
Caused by: java.io.IOException: Permission denied
at Lab.readFile(Lab.java:14)
at Lab.load(Lab.java:19)
... 2 more
Every Caused by block is a complete exception with its own stack, hanging off the one above it. This is the caused by java stack trace shape you'll meet in every real service: an exception at the top that nobody wanted to see, and the one that matters at the bottom. The full reading routine for any trace lives in How to Read a Java Stack Trace. This article is about one thing in it: the chain.
The two-second version
The last Caused by is the root cause. Here it's IOException: Permission denied, thrown at Lab.readFile(Lab.java:14). Everything above it is somebody catching that and wrapping it in something more fitting for their layer.
My first instinct, and I suspect yours, was to open readFile:14 and stop. Wrong half the time. Look at what that message doesn't say: which file. Permission denied, and nothing else. The path is in the wrapper's message, cannot load /etc/app/app.properties, two exceptions up. So the answer is: the bottom tells you what, the top tells you what it was doing at the time, and you need both lines.
Reproduce it
Pinned JDK: OpenJDK 21.0.12.1. No libraries. Save the file below as Lab.java, and keep the blank lines, because every line number in this article depends on them.
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.io.StringReader;
public class Lab {
static class ConfigException extends RuntimeException {
ConfigException(String msg, Throwable cause) { super(msg, cause); }
ConfigException(String msg) { super(msg); }
}
static String readFile(String path) throws IOException {
throw new IOException("Permission denied"); // the real problem
}
static Properties load(String path) {
try {
String text = readFile(path); // line 19: where readFile is called
Properties p = new Properties();
p.load(new StringReader(text));
return p;
} catch (IOException e) {
throw new ConfigException("cannot load " + path, e); // line 24: wrap, keep the cause
}
}
static Properties loadLossy(String path) {
try {
return load(path); // line 30
} catch (ConfigException e) {
throw new RuntimeException(e.getMessage()); // line 32: message copied, cause dropped
}
}
static Properties loadWrapped(String path) {
try {
return load(path); // line 38
} catch (ConfigException e) {
throw new IllegalStateException(e); // line 40: Throwable(Throwable) constructor
}
}
static Throwable root(Throwable t) {
List<Throwable> seen = new ArrayList<>();
while (t.getCause() != null && !seen.contains(t.getCause())) {
seen.add(t);
t = t.getCause();
}
return t;
}
public static void main(String[] args) throws Exception {
String mode = args.length > 0 ? args[0] : "chain";
switch (mode) {
case "chain" -> loadWrapped("/etc/app/app.properties"); // line 56
case "lossy" -> loadLossy("/etc/app/app.properties"); // line 57
case "root" -> {
try { loadWrapped("/etc/app/app.properties"); }
catch (Exception e) { System.out.println("root = " + root(e)); }
}
case "cycle" -> {
Exception a = new Exception("a");
Exception b = new Exception("b", a);
a.initCause(b); // line 65
a.printStackTrace();
System.out.println("root(a) = " + root(a));
}
case "initcause" -> {
Exception x = new Exception("x", null);
x.initCause(new Exception("y")); // line 71: cause already set (to null)
}
case "thread" -> {
Thread t = new Thread(() -> { throw new ConfigException("in thread", new IOException("disk")); }, "loader-1");
t.start();
}
}
}
}
Compile it, then run the modes one at a time:
javac -d out Lab.java
java -cp out Lab chain # the trace at the top of the page
java -cp out Lab lossy # cause dropped
java -cp out Lab root # getCause() loop
java -cp out Lab cycle # two exceptions that blame each other
java -cp out Lab initcause # overwriting a cause
java -cp out Lab thread # cause with no frames of its own
Compile with javac; don't use the java Lab.java source launcher. I wasted time on that in the first article already: the launcher trims the frames of the outer exception but not the cause, so ... N more lines come out different from what a normal run prints. If your console shows a Picked up JAVA_TOOL_OPTIONS line first, that's stderr from your environment, nothing to do with the exception.
Here's lossy:
Exception in thread "main" java.lang.RuntimeException: cannot load /etc/app/app.properties
at Lab.loadLossy(Lab.java:32)
at Lab.main(Lab.java:57)
Message intact. Where it came from: gone. The permission problem isn't anywhere in this output, and nothing you do to the log configuration will bring it back, because the information never left the JVM's heap.
Every line, annotated
Exception in thread "main" java.lang.IllegalStateException: Lab$ConfigException: cannot load ... (1)
at Lab.loadWrapped(Lab.java:40) (2)
at Lab.main(Lab.java:56)
Caused by: Lab$ConfigException: cannot load /etc/app/app.properties (3)
at Lab.load(Lab.java:24) (4)
at Lab.loadWrapped(Lab.java:38) (5)
... 1 more (6)
Caused by: java.io.IOException: Permission denied (7)
at Lab.readFile(Lab.java:14) (8)
at Lab.load(Lab.java:19) (9)
... 2 more (10)
(1) The outermost exception, with a message that looks like it repeats itself. It does. Line 40 used new IllegalStateException(e), the constructor that takes only a cause, and that constructor builds its message from cause.toString(): class name, colon, message. So the whole Lab$ConfigException: cannot load ... text is pasted into the first line. Not a bug; a habit worth knowing, since it's why so many log lines start with an exception name inside an exception name.
(2) Frames for this one exception only, newest first. Line 40 is where the IllegalStateException was constructed.
(3) Caused by: introduces the exception that getCause() returns on the one above. Same format as a top-level trace, minus the thread name.
(4) and (5) Two frames. Lab.load at line 24 is where the ConfigException was built. Lab.loadWrapped at line 38 is where load was called from.
(6) ... 1 more. One frame at the bottom of this block, Lab.main(Lab.java:56), is identical to the bottom of the trace above it, so the printer skips it. It's one frame, and it's the one printed on line 3 of the output.
(7) The root cause. The bottom of the chain. No further Caused by follows, which means getCause() returned null.
(8) and (9) The first frame is where new IOException(...) ran. The second is the call that led there, at Lab.load(Lab.java:19). Note it's line 19 here and line 24 in frame (4): the same method, caught at two different statements, and that matters in a second.
(10) ... 2 more. Two frames hidden this time. Which two? Count from the bottom of both stacks. The IOException's full stack is four frames: readFile:14, load:19, loadWrapped:38, main:56. I confirmed that by printing getStackTrace() for each exception in the chain. The ConfigException's is load:24, loadWrapped:38, main:56. Starting from the bottom, main:56 matches, loadWrapped:38 matches, and load:19 against load:24 does not. Two in common. Stop.
Which line would I open first? Lab.load(Lab.java:24), because that's where our own code decided what to say about the failure. Then readFile:14 for the cause itself.
Inside printStackTrace: how the chain gets printed
None of this is stored in the exception as text. The stack is an array of frames captured at construction. The Caused by structure is nothing more than a field called cause, and the printer walks it at print time.
I read the printing code (OpenJDK Throwable.java at jdk-21+35) through a summarising fetch tool, not line by line in an editor, so take the details below as what that read showed and the experiments confirmed. The private method that prints a chained exception takes a caption ("Caused by: " or "Suppressed: "), an indent prefix, and the stack trace of the exception that holds this one. It walks both arrays from the end while frames compare equal, and the number it ends up with is the N in ... N more. Frames before the first mismatch are printed; the shared ones are counted and replaced by that one line.
Two consequences I didn't expect.
First, "equal" includes the line number. A StackTraceElement is the same only if class, method, file and line match. So a wrapper built on a different line of the same method than the call that threw (very common) ends the shared stretch early. That's what happened to load:19 versus load:24.
Second, the printer keeps an identity-based set of every throwable it has already printed. If a cause chain loops back, it doesn't recurse forever. It prints a marker instead. Run cycle:
java.lang.Exception: a
at Lab.main(Lab.java:63)
Caused by: java.lang.Exception: b
at Lab.main(Lab.java:64)
Caused by: [CIRCULAR REFERENCE: java.lang.Exception: a]
root(a) = java.lang.Exception: b
Exception b was built with a as its cause, then line 65 told a that its cause was b. Nothing stops you; the cause of a hadn't been set yet. This is rare in the wild, and I only built it to see what the JVM does. The printer survives. A naive while (e.getCause() != null) e = e.getCause(); would not, which is why my root() helper remembers what it has seen. Its answer here (b) is arbitrary; a cycle has no root.
One more oddity. getCause() doesn't return the raw field. A new Throwable starts with cause pointing at itself, as a "never set" marker, and getCause() turns that into null. That's why this:
Exception in thread "main" java.lang.IllegalStateException: Can't overwrite cause with java.lang.Exception: y
at java.base/java.lang.Throwable.initCause(Throwable.java:464)
at Lab.main(Lab.java:71)
Caused by: java.lang.Exception: x
at Lab.main(Lab.java:70)
happens in the initcause mode. new Exception("x", null) is explicitly "no cause", which counts as set, so a later initCause is refused. The Caused by: x is the JDK attaching the original exception to the complaint. initCause exists for exceptions whose constructors can't take a cause; if you control the class, use the constructor. (Throwable.java:464 is specific to 21.0.12.1.)
The print format is old. The Caused by and ... N more style dates back to JDK 1.4, and as far as I know it's unchanged. I couldn't run JDK 8, 17 or 25 here; this lab ran on 21 only. There's even a long-open request, JDK-6507809, arguing the whole format is hard to read and should be redone. It hasn't been.
It took me two sittings to get to the bottom of this one, most of it on the thread case further down. You get to skip that.
The Caused by with no frames, and other near-misses
Run thread:
Exception in thread "loader-1" Lab$ConfigException: in thread
at Lab.lambda$main$0(Lab.java:74)
at java.base/java.lang.Thread.run(Thread.java:1583)
Caused by: java.io.IOException: disk
... 2 more
A Caused by with no frames at all, only ... 2 more. My first guess was a lost trace. I went looking for a flag. Nothing was lost. Both exceptions were built on line 74, in the same lambda, in the same call stack, so every frame of the cause matches the bottom of the outer trace and the printer folds all of them. The cause is entirely shared. If you see this, the wrapper and the thing it wraps were created at the same spot, usually new X("...", new Y("...")) on one line.
The one that actually costs people time is the wrong "root". Three more:
Blaming the last at of the top block. That's the call stack of the wrapper, not the failure. The failure is in the last block.
Assuming the bottom exception is always the useful one. Our IOException had no path. If the bottom had been NullPointerException with no message, the outer exception's text would be the only business context you'd get. Read both.
Believing ... 2 more means two frames are missing. They're printed, once, in the block above. It's the same myth as in the hub article, and I'll keep repeating it until it dies.
And a fourth, from the wrapper that eats its cause. In lossy, someone wrote new RuntimeException(e.getMessage()). There's no Caused by block, and no way to get one back. If you inherit a codebase with that pattern, grep for getMessage()) inside throw new first.
Write it so it explains itself
Pass the cause. Every time. new ConfigException("cannot load " + path, e) on line 24 costs nothing and it's the only reason the chain printed at all. The single-argument new IllegalStateException(e) on line 40 works too, but gives you that doubled message. I prefer to pass a message and the cause so the first line says what the code was doing.
Put the identifying value in the wrapper's message, since the root cause often doesn't have it. Costs: a longer log line, and care not to put secrets (tokens, account numbers) in the text. I'll come back to that in Writing Java Exceptions That Produce Useful Stack Traces.
Finding the root programmatically? My root() loop above is fine for a log-enrichment helper, with the cycle guard. Libraries ship the same thing (Apache Commons Lang's ExceptionUtils.getRootCause is the usual one; I linked it but didn't read its source). Don't build alerting logic that only looks at the root class, though. A SQLException root can be a deadlock or a typo in a column name, and the wrapper above it is the thing that knows which operation was running.
What isn't a fix: logging at every layer. Each catch that logs and rethrows produces its own copy of the chain, and now the same IOException is in your logs four times with slightly different ... N more counts. Log once, at the boundary.
Keeping the chain alive in your logs
The chain survives only if the logger receives the Throwable itself. log.error("load failed: " + e) keeps the exception's toString() and drops Caused by, the same way the lossy mode did. Pass the exception as the last argument and the logging framework prints the chain. Logging Java Exceptions Properly covers that and what happens when a log shipper splits it across thirty events.
Logback and Log4j2 both have pattern options to shorten the chain or print the root cause first. I didn't run either for this article, so check the docs for the version you use; don't copy a pattern from a blog post (including this one) without testing what it does to your ... N more lines. Some of them reorder frames in a way that makes the elision harder to follow.
Which JDKs print what, from what I could verify: the chain format above is from JDK 21.0.12.1 and I ran nothing else. Caused by, ... N more and the circular-reference marker are, as far as I know, stable on every recent LTS, but I haven't watched them on 8, 17 or 25, so I'm not putting that in writing as fact. The module prefix (java.base/) in Throwable.initCause is a JDK 9+ thing and will be absent on 8.
If you remember nothing else
- The last
Caused byis the root cause, and the outer message is often where your business context lives. Read both ends. ... N morecounts frames shared with the enclosing trace from the bottom up, stopping at the first mismatch, line numbers included. Nothing is lost.- A
Caused bywith only... N moremeans wrapper and cause were created in the same place. new X(e.getMessage())throws the chain away.new X(msg, e)keeps it.- Walking
getCause()needs a cycle guard;printStackTracehas one, your loop doesn't.
People also ask
What does "Caused by" mean in a Java stack trace?
It starts the stack trace of the exception that getCause() returned for the one above. The code caught a low-level failure and threw a new exception that carries the first as its cause. Verified on JDK 21.
How do I find the root cause in a long Java stack trace?
Scroll to the last Caused by: block. It's the exception with no cause of its own. Programmatically, loop on getCause() until it returns null, and remember values you've seen so a cycle can't hang you. See the loop in the lab above.
What does "... 23 more" mean in a stack trace?
Twenty-three frames at the bottom of this block are identical to the bottom 23 of the enclosing exception, so they aren't printed twice. Count up from the bottom of both until a frame differs; the line number counts. The basics are in the hub article.
Why is my Caused by missing?
Someone threw a new exception without passing the old one in, for instance new RuntimeException(e.getMessage()). The cause was never stored, so no logger can print it.