💥 Exceptions & Errors · Intermediate

Exception chaining in Java

Wrapping causes with new X(msg, cause); getCause.

🧩 The mysteryYour logs say "save failed". Nothing else. Somewhere a precise SQL error was thrown away by one missing argument. Which one?

Translate, don't erase

When you catch a low-level exception and throw a higher-level one, pass the original as the cause: new DataException(msg, e). **getCause() returns the wrapped exception, and stack traces print it under Caused by:**.

try {
    return repo.load(id);
} catch (SQLException e) {
    throw new DataException("load " + id, e);
}
🔮 Predict it

Two messages

What does this print?

try {
    try {
        Integer.parseInt("12a");
    } catch (NumberFormatException e) {
        throw new RuntimeException("port", e);
    }
} catch (RuntimeException e) {
    System.out.println(e.getMessage());
    System.out.println(e.getCause().getMessage());
}
  1. port For input string: "12a"
  2. port null
  3. For input string: "12a" port
Show the answer

The outer exception has its own message, port, and keeps the NumberFormatException as its cause — with the original message intact.

The Throwable toolkit

**getMessage() — the detail text. getCause() — the exception this one wraps, or null** if no cause was given (via constructor or initCause). **getSuppressed() — exceptions attached during cleanup. getStackTrace()** — the call-site frames as an array. Together they tell the whole story.

⚠️ The trap

The missing , e

Forget to pass e and the new exception has no cause: the SQL details and their stack trace are thrown away. One argument makes all the difference.

} catch (SQLException e) {
    throw new DataException("load " + id);
    // fix: new DataException("load " + id, e)
}
🔮 Predict it

Down to the root

What does this print?

Throwable t = new RuntimeException("app",
    new IllegalStateException("db",
        new IOException("disk")));
while (t.getCause() != null) {
    t = t.getCause();
}
System.out.println(t.getMessage());
  1. disk
  2. app
  3. db
Show the answer

The loop walks the chain until an exception with no cause: the innermost IOException, whose message is disk.

💼 In the real world

Layered apps

A repository wraps SQLException in DataException; a service wraps that in OrderFailedException. Each layer speaks its own vocabulary, and the log still shows Caused by: all the way down to "connection refused" — so the on-call engineer knows exactly what broke.

Key takeaways

  1. new X(msg, cause), or initCause(cause)
  2. getCause() returns the wrapped Throwable, or null
  3. Translate low-level errors into domain errors without losing detail
  4. Stack traces print 'Caused by:' chains

💡 A doctor's referral letter that includes the original test results.

🤯 Did you know?

Exception chaining — getCause() and the cause constructors — only arrived in Java 1.4 (2002). Before that, each library invented its own way to wrap exceptions.

Practice questions

What does this print?

try {
    try {
        Integer.parseInt("abc");
    } catch (NumberFormatException e) {
        throw new IllegalStateException("config", e);
    }
} catch (IllegalStateException e) {
    System.out.println(e.getMessage());
    System.out.println(e.getCause().getMessage());
}
  1. config For input string: "abc"
  2. config null
  3. For input string: "abc" config
  4. Throws NullPointerException
Check your answer

config For input string: "abc". The outer exception has its own message, "config", and keeps the NumberFormatException as its cause — with the original message intact.

What does this print?

Throwable t = new RuntimeException("top",
    new IllegalStateException("mid",
        new ArithmeticException("root")));
while (t.getCause() != null) {
    t = t.getCause();
}
System.out.println(t.getMessage());
  1. root
  2. top
  3. mid
  4. null
Check your answer

root. The loop walks the chain until an exception with no cause: the innermost ArithmeticException, whose message is "root".

Next: the wall of red text in your console. Learn to read a stack trace in ten seconds.