Throwable Hierarchy: Error vs Exception in Java

Understand Java's Throwable hierarchy, the distinction between Error and Exception, and when each category should be handled differently.

published: reading time: 14 min read author: Geek Workbench

Throwable Hierarchy: Error vs Exception in Java

Java’s exception handling framework is built on a single root class: Throwable. Everything that can be thrown in Java inherits from this type, whether it is a recoverable exception or a fatal virtual machine error.

Introduction

The Throwable hierarchy is the foundation of everything Java does with exceptions — from the NullPointerException every developer encounters to the StackOverflowError that terminates a thread and the IOException that signals a file read failure. Understanding the hierarchy means understanding the distinction between what you should catch and handle versus what you should never catch and should let terminate the process.

The hierarchy splits at Throwable into Error and Exception. Error represents fatal JVM conditions — things so wrong that the program cannot safely continue. OutOfMemoryError, StackOverflowError, and InternalError indicate systemic failures in the runtime environment, not failures in your logic. Catching these is almost always wrong because the program is already in an invalid state. Exception represents recoverable failures — the file was not found, the network connection was refused, the user typed invalid input. These are the failures that callers can reasonably be expected to handle.

The Exception branch further splits into checked exceptions (subclasses of Exception that are not RuntimeException) and unchecked exceptions (RuntimeException and its subclasses). Checked exceptions are enforced by the compiler — the caller must acknowledge them via catch or throws declaration. Unchecked exceptions are not compiler-enforced. The rule of thumb: external failures that the caller can reasonably handle (I/O, network, invalid input) are checked; programming bugs that indicate invalid program state (null dereference, index out of bounds, illegal argument) are unchecked.

This guide covers the full hierarchy, the checked/unchecked distinction and when each applies, the common mistakes of catching Error or generic Throwable, and the implications for exception handling design in application code.

When to Use

You will encounter the Throwable hierarchy when:

  • Catching built-in Java exceptions (NullPointerException, IllegalArgumentException)
  • Distinguishing recoverable failures from unrecoverable errors
  • Understanding framework error handling behavior
  • Designing application-specific exception types
try {
    Integer.parseInt("not-a-number");
} catch (NumberFormatException e) {
    // Handle recoverable input validation error
    System.err.println("Invalid number format: " + e.getMessage());
}

When NOT to Use

Avoid these common mistakes:

  • Do not catch Error — JVM errors like StackOverflowError indicate fatal conditions that cannot be meaningfully handled
  • Do not throw Error subclasses — Use RuntimeException or custom exceptions for business logic failures
  • Do not catch generic Throwable in application code — This masks all problems including AssertionErrors and JVM bugs
  • Do not confuse checked vs unchecked — Catching Exception (the checked supertype) does not catch runtime exceptions

Throwable Hierarchy Diagram

classDiagram
    class Throwable {
        +String message
        +Throwable cause
        +void printStackTrace()
        +String getMessage()
        +Throwable getCause()
    }
    class Error {
        <<Error>>
        +void printStackTrace()
    }
    class Exception {
        <<Exception>>
        +void printStackTrace()
    }
    class RuntimeException {
        <<RuntimeException>>
        +void printStackTrace()
    }
    class IOException {
        <<checked>>
        +void printStackTrace()
    }
    class SQLException {
        <<checked>>
        +void printStackTrace()
    }
    class NullPointerException {
        <<RuntimeException>>
        +void printStackTrace()
    }
    class IllegalArgumentException {
        <<RuntimeException>>
        +void printStackTrace()
    }
    class OutOfMemoryError {
        <<Error>>
        +void printStackTrace()
    }
    class StackOverflowError {
        <<Error>>
        +void printStackTrace()
    }

    Throwable <|-- Error
    Throwable <|-- Exception
    Exception <|-- RuntimeException
    Exception <|-- IOException
    Exception <|-- SQLException
    RuntimeException <|-- NullPointerException
    RuntimeException <|-- IllegalArgumentException
    Error <|-- OutOfMemoryError
    Error <|-- StackOverflowError

Hierarchy Explained

TypeCategoryChecked?Typical Use
ThrowableBase classRoot of all throwable types
ErrorJVM errorsNoFatal conditions (OutOfMemory, StackOverflow)
ExceptionRecoverable failuresYes/NoDepends on subclass
RuntimeExceptionProgramming errorsNoBugs (NPE, illegal arguments)
IOExceptionI/O failuresYesExternal resource failures
SQLExceptionDatabase failuresYesJDBC operation failures

Checked vs Unchecked

Checked exceptions (subclasses of Exception except RuntimeException) must be either caught or declared in the method signature with throws. The compiler enforces this, making them suitable for recoverable external failures.

Unchecked exceptions include RuntimeException and Error subclasses. The compiler does not require handling, and these generally represent programming bugs or fatal JVM states.

// Checked — compiler requires handling
public void readFile(String path) throws IOException {
    Files.readString(Path.of(path));
}

// Unchecked — no compiler enforcement
public void divide(int a, int b) {
    if (b == 0) throw new ArithmeticException("Division by zero");
}

Failure Scenarios

// Scenario 1: Catching Error (WRONG)
try {
    recursiveMethod();
} catch (Error e) {
    // BAD: Errors are fatal, cannot recover
    System.out.println("Caught error: " + e);
}

// Scenario 2: Catching generic Throwable (WRONG)
try {
    riskyOperation();
} catch (Throwable t) {
    // BAD: Catches AssertionError, OutOfMemoryError, JVM bugs
    // Masks real problems
}

// Scenario 3: Catching checked vs unchecked (CORRECT)
try {
    Integer.parseInt("abc");
} catch (NumberFormatException e) {
    // GOOD: Specific unchecked exception for input validation
    System.out.println("Invalid input: " + e.getMessage());
}

Trade-off Table

ApproachProsCons
Catch ExceptionCatches all exceptionsAlso catches RuntimeException
Catch specific typesPrecise handlingMay miss edge cases
Catch ThrowableCatches everythingCatches Errors, masks bugs
Propagate uncheckedCaller handles only what mattersUndocumented failure modes
Propagate checkedCompiler enforces handlingVerbose signatures

Security Notes

  • Do not expose stack traces in productionprintStackTrace() writes to standard error, which may be logged to files accessible to attackers
  • Sanitize exception messages before logging — Do not include passwords, session tokens, or PII in error messages
  • Avoid exception tunneling — Converting checked exceptions to unchecked without documenting the failure mode obscures error handling
  • Failure to catch Error — In server applications, uncaught errors can cause thread death without proper cleanup
// SECURE: Log without exposing stack trace details
try {
    processUserData();
} catch (Exception e) {
    logger.error("User data processing failed: {}", e.getClass().getName());
    // Do NOT log: e.getMessage() may contain sensitive data
}

Common Pitfalls

  1. Swallowing exceptions silently — Empty catch blocks hide failures
  2. Catching Exception too broadly — Masks programming bugs
  3. Re-throwing without context — Original exception lost in stack trace
  4. Confusing Error with Exception — Error should not be caught
  5. Over-relying on checked exceptions — Creates verbose signatures and tight coupling

Quick Recap

  • Throwable is the root of all throwable types
  • Error represents JVM fatal conditions — do not catch
  • Exception represents recoverable failures
  • RuntimeException is unchecked — indicates programming bugs
  • Checked exceptions require handling or declaration; unchecked do not
  • Never catch Error or generic Throwable in application code

Interview Questions

1. What is the difference between Error and Exception in Java?

Model Answer: "Exception and Error both extend Throwable. Exception represents recoverable failures that can be caught and handled — like IOException or SQLException. Error represents fatal JVM conditions — like OutOfMemoryError or StackOverflowError — that applications should not attempt to catch. Errors indicate the program itself is in an invalid state."

2. What are checked and unchecked exceptions?

Model Answer: "Checked exceptions are subclasses of Exception but not RuntimeException. The compiler requires them to be caught or declared with throws. They represent recoverable external failures like file not found. Unchecked exceptions include RuntimeException and Error subclasses. The compiler does not require handling. RuntimeException typically indicates programming bugs like null pointers or invalid arguments."

3. Why should you avoid catching generic Throwable?

Model Answer: "Catching Throwable catches both Exception and Error. Errors are fatal JVM conditions that cannot be meaningfully handled, and catching them masks real bugs. Additionally, catching AssertionError (which extends Error) would interfere with test frameworks. Always catch the most specific type possible."

4. What is the Throwable hierarchy in Java?

Model Answer: "The hierarchy is: Throwable → Exception (checked) and Throwable → Error. RuntimeException extends Exception. Common unchecked exceptions like NullPointerException and IllegalArgumentException extend RuntimeException. Common errors include OutOfMemoryError and StackOverflowError which extend Error."

5. Should you catch OutOfMemoryError?

Model Answer: "Generally no. OutOfMemoryError indicates the JVM is exhausted and the program cannot safely continue. Even if caught, attempting to allocate new objects will likely fail again. The appropriate response is to let the error propagate, log it for diagnostics, and terminate gracefully if possible. Some long-running servers use custom allocators to handle partial failures, but this is not typical application code."

6. Can you catch both Error and Exception in the same catch block?

Model Answer: "Yes, by catching Throwable, which is the common supertype of both Error and Exception. However, this is almost always wrong in application code because it masks fatal JVM errors like OutOfMemoryError and StackOverflowError. Only use this pattern if you are writing low-level infrastructure code that needs to handle all possible failure modes."

7. How does the compiler enforce checked exception handling?

Model Answer: "The compiler tracks checked exceptions — subclasses of Exception that are not RuntimeException subclasses. When a method throws a checked exception, either the caller must catch it in a try-catch block, or the method must declare it in its throws clause. Without this handling, code fails to compile. This enforcement ensures that recoverable failures like I/O errors are not accidentally ignored."

8. What is the difference between getMessage() and getLocalizedMessage()?

Model Answer: "getMessage() returns the string passed to the exception constructor, useful for debugging. getLocalizedMessage() is intended for locale-specific messages by default it delegates to getMessage() but subclasses can override it to provide localized error descriptions for end users."

9. Why does RuntimeException not require throws declaration?

Model Answer: "RuntimeException is an unchecked exception — the compiler does not enforce handling or declaration requirement for any subclasses of RuntimeException or Error. This is because RuntimeException typically represents programming bugs (NPE, illegal arguments, index out of bounds) that indicate the program is in an invalid state and should not be recovered from at runtime. Forcing declaration would pollute method signatures with implementation details."

10. What happens when you catch an exception but do not handle it?

Model Answer: "If a catch block catches an exception but performs no action — either no code or just a comment — the exception is silently swallowed. The program continues as if no exception occurred, which corrupts state and makes bugs hard to diagnose. A catch block should either recover from the failure, log the exception and rethrow, or wrap and rethrow with added context."

11. Can an exception be thrown from a static initializer?

Model Answer: "Yes. If a static initializer throws an exception — checked or unchecked — the class is marked as uninitialized. Any attempt to use that class results in an ExceptionInInitializerError (which wraps the original exception). This prevents the class from being used in an invalid state. The wrapped exception can be retrieved via getCause() on the ExceptionInInitializerError."

12. How does exception propagation work with inheritance?

Model Answer: "When overriding a method, the overriding method cannot declare new checked exceptions beyond those declared in the parent. It can throw a narrower exception type (a subtype of the parent's exception) or throw none. This ensures that code using the parent type reference still handles all possible exceptions. If a parent declares throws IOException, the child can throw FileNotFoundException but not SQLException."

13. What is the relationship between RuntimeException and Exception?

Model Answer: "RuntimeException extends Exception. All RuntimeException subclasses are unchecked — the compiler does not require handling or declaration. The Exception branch splits into checked exceptions (RuntimeException's siblings) and unchecked RuntimeExceptions. This is why catching Exception catches both checked exceptions and RuntimeExceptions unless you specifically exclude RuntimeException."

14. Can you have a try block without catch or finally?

Model Answer: "Yes. A bare try block with only a finally (no catch) is valid — cleanup runs regardless of what happens in the try block, and the exception propagates if not caught. This is equivalent to the pattern used by try-with-resources before Java 7. The try block must be followed by at least one catch or a finally."

15. How does Throwable relate to the catching hierarchy?

Model Answer: "The catch block argument type determines which exceptions can be caught. A catch(IOException e) block catches IOException and any subclass (FileNotFoundException, SocketException). A catch(Exception e) block catches all exceptions including RuntimeExceptions. A catch(Throwable t) block catches everything including Error subclasses."

16. What is ExceptionInInitializerError and when does it occur?

Model Answer: "ExceptionInInitializerError wraps an exception thrown during class initialization (static initializers or static field initializers). It prevents the class from being used because initialization failed. The original exception is available via getCause(). This error type itself extends LinkageError, not Exception, meaning it indicates a programming environment problem rather than a runtime failure in normal program logic."

17. Is NullPointerException checked or unchecked?

Model Answer: "NullPointerException is unchecked. It extends RuntimeException, which is a subclass of Exception but not a checked exception. The compiler does not require handling or declaration. NullPointerException indicates a programming bug — typically dereferencing null — and is not considered a recoverable external failure."

18. What is the fillInStackTrace() method and when is it used?

Model Answer: "fillInStackTrace() populates the stack trace for the exception by capturing the current execution stack. It is called automatically when an exception is constructed. You rarely need to call it directly, but doing so before re-throwing an exception transfers the stack trace from the original throw point to the re-throw point — which can hide the real origin of the error. Only use it deliberately."

19. Why should you not catch Error in production code?

Model Answer: "Error subclasses (StackOverflowError, OutOfMemoryError, InternalError) indicate fatal JVM conditions. The program is in an invalid state and cannot safely recover. Catching these errors masks the problem and lets execution continue into potentially dangerous territory. Even if you catch OutOfMemoryError, for example, allocating new objects will likely fail again. Let errors propagate and terminate the process cleanly."

20. How do you choose between checked and unchecked exceptions?

Model Answer: "Use checked exceptions (extends Exception, not RuntimeException) for recoverable external failures that the caller is expected to handle — file not found, connection refused, invalid user input. Use unchecked exceptions (extends RuntimeException) for programming bugs and invalid states that should not occur in correct programs — null dereference, illegal argument, index out of bounds. When in doubt, prefer unchecked — checked exceptions create verbose APIs and tight coupling."

Further Reading

Summary

The Throwable hierarchy is the foundation of Java’s exception handling system. At the root sits Throwable, branching into Error for fatal JVM conditions that should never be caught, and Exception for recoverable failures. The checked/unchecked distinction within Exception determines whether the compiler enforces handling — checked exceptions like IOException and SQLException represent external failures, while RuntimeException and its subclasses indicate programming bugs.

Understanding which type to catch and when is critical. Never catch Error or generic Throwable in application code — this masks fatal conditions and makes debugging impossible. When designing exception handling, prefer catching specific types over broad categories. If you are handling exceptions in practice, review the Try-Catch-Finally patterns to ensure cleanup code runs correctly, and consider Custom Exceptions when you need domain-specific error signaling beyond what built-in types provide.

Category

Related Posts

Abstract Classes in Java

Learn about partially implemented classes that define contracts for subclasses using abstract methods and concrete implementations.

#java-abstract-classes #java #java-fundamentals

Arithmetic Operators in Java

Master Java arithmetic operators: addition, subtraction, multiplication, division, and modulo with integer division gotchas and operator precedence explained.

#java-arithmetic-operators #java #java-fundamentals

Array Basics in Java

Learn Java array fundamentals: declaration, initialization, element access, and the length property explained simply.

#java-array-basics #java #java-fundamentals