Question 5 · Section 7

What is at the top of the exception hierarchy?

At the very top of the hierarchy of all objects that can be thrown and caught is the class java.lang.Throwable.

Language versions: English Russian Ukrainian

Junior Level

Root of the hierarchy - java.lang.Throwable

At the very top of the hierarchy of all objects that can be thrown and caught is the class java.lang.Throwable.

Object
  └── Throwable
        ├── Error (fatal JVM errors)
        │     ├── OutOfMemoryError
        │     ├── StackOverflowError
        │     └── NoClassDefFoundError
        │
        └── Exception (application errors)
              ├── RuntimeException (unchecked)
              │     ├── NullPointerException
              │     └── IllegalArgumentException
              │
              └── Checked Exceptions
                    ├── IOException
                    └── SQLException

Two main branches

Error - fatal JVM problems:

  • OutOfMemoryError - ran out of memory
  • StackOverflowError - infinite recursion
  • Don’t catch them - system is in an unstable state

Exception - application errors:

  • Can and should be handled
  • Divided into checked and unchecked

Error - problems that the application cannot fix (memory, stack). Exception - problems that it can. The separation in the hierarchy allows catching Exception without catching Error.

Example

// Throwable - parent of all exceptions
try {
    // some code
} catch (Throwable t) {
    // Catches EVERYTHING, including Error (usually you don't do this)
    System.err.println("Caught: " + t.getMessage());
}

Middle Level

Why does Throwable implement Serializable?

Serialization - converting an object to bytes for network transmission or storage. This is needed for RMI (remote method invocation), JMX, caching. An exception must be serializable to “travel” between JVMs.

This is critical for distributed systems (RMI, JMX). When an exception occurs on the server, the JVM serializes it along with the stack trace and sends it to the client.

Key subclasses of Error

  • OutOfMemoryError - different subtypes:
    • Java heap space - heap is full
    • Metaspace - no memory for class metadata
    • Unable to create new native thread - OS denied threads
  • ThreadDeath - thrown on Thread.stop() (deprecated). This is an Error so that a normal catch (Exception e) doesn’t swallow it.

  • NoClassDefFoundError - class was present at compile time but missing at runtime. Environment integrity is broken.

The problem of propagation through ExecutorService

When an exception occurs inside a pool thread, it is caught by ExecutorService and stored inside the Future:

Future<?> future = executor.submit(() -> {
    throw new RuntimeException("Task failed!");
});

// Exception is "hidden" inside Future
try {
    future.get(); // ExecutionException is thrown here
} catch (ExecutionException e) {
    System.err.println("Cause: " + e.getCause()); // RuntimeException
}
// ExecutorService catches all exceptions from tasks and stores them inside Future.
// When future.get() is called, the exception is wrapped in ExecutionException.
// To get to the cause: e.getCause().

fillInStackTrace()

fillInStackTrace() - a native method (written in C/C++, not Java). It walks the call stack at the OS level, creating a snapshot of all frames. This is expensive: requires transition between Java and native code + walking the entire stack.

When getStackTrace() is called, the native structure is converted to StackTraceElement[] array - this is a lazy optimization, but filling the native structure is still expensive.


Senior Level

Under the Hood: hidden backtrace field

Inside Throwable there is a field private Object backtrace, filled by the native method fillInStackTrace(). It holds a reference to the native call stack in JVM memory.

Serialization Pitfall: during serialization, the native backtrace is lost, and the JVM converts it to an array of Java objects. This increases packet size and CPU load.

Disabling stack trace for performance

public class FastException extends RuntimeException {
    public FastException(String message) {
        // enableSuppression = true, writableStackTrace = false
        super(message, null, true, false);
    }
}

Speeds up creation by tens of times - native stack walk is not executed.

Immutable Exceptions

In high-load systems, static instances are created:

public static final MyException INVALID_DATA =
    new MyException("Invalid data", null, true, false);

Does not create new objects, but logs will have identical stack traces.

Suppressed Exceptions

Introduced in Java 7 for try-with-resources. If an exception occurs in try, and a second one - when closing the resource in finally, the first becomes the main one, the second is added as suppressed:

try {
    resource.doWork(); // IOException #1
} finally {
    resource.close();  // IOException #2
}
// IOException #1 - main, #2 - suppressed

Diagnostics

  • Never do catch (Throwable t) in business logic - will catch OutOfMemoryError
  • Root Cause Analysis - use ExceptionUtils.getRootCause() to find the root cause
  • Distributed systems - at the service boundary, extract the message and error code, pass only DTO

Interview Cheat Sheet

Must know:

  • Root of hierarchy - java.lang.Throwable (not Exception)
  • Two branches: Error (fatal JVM problems) and Exception (application errors)
  • Error - OutOfMemoryError, StackOverflowError, NoClassDefFoundError - don’t catch
  • Exception is divided into RuntimeException (unchecked) and checked exceptions
  • fillInStackTrace() - native method, walks call stack, expensive operation
  • Suppressed exceptions - Java 7 mechanism for try-with-resources (saves second exception)
  • Disabling stack trace (writableStackTrace = false) speeds up creation by tens of times
  • During serialization, native backtrace is lost - converted to Java objects

Frequent follow-up questions:

  • Why does Throwable implement Serializable? - For RMI, JMX, caching - exceptions “travel” between JVMs
  • What happens if you catch OutOfMemoryError? - JVM may be in unstable state, next operation will fail
  • What are suppressed exceptions? - When try and close() both threw two exceptions, the second is added to the suppressed list of the first
  • Why not create immutable exceptions for everything? - Identical stack trace makes debugging harder

Red flags (NOT to say):

  • “Root of hierarchy is Exception” - no, Throwable
  • “I catch Throwable in business logic for reliability” - it will catch OutOfMemoryError
  • “fillInStackTrace() is a normal Java method” - no, it’s native, walks stack at OS level

Related topics:

  • [[What is Throwable]]
  • [[What is the difference between Error and Exception]]
  • [[What are suppressed exceptions]]
  • [[What is a stack trace]]
  • [[What is try-with-resources]]