Java Tutorial
🔍

Java Exception Hierarchy

Java Exception Hierarchy

The Java exception hierarchy is a class tree rooted at java.lang.Throwable. Every object that can be thrown — whether it is a null pointer dereference, a missing file, or a heap exhaustion — is an instance of some class in this tree. The position of an exception class in that tree determines three things: whether the compiler forces you to handle it, which catch block intercepts it, and what it signals about the severity of the problem. Knowing the hierarchy is not just academic preparation — it explains every catch block ordering rule, every compiler error about unhandled exceptions, and every design decision behind custom exception classes.

What Is the Java Exception Hierarchy?

The hierarchy begins with java.lang.Throwable. Everything below it inherits the core state that makes exceptions useful: a human-readable message, a reference to the exception that caused this one, and the stack trace captured at the moment the exception was created.

java.lang.Object
    └── java.lang.Throwable
            ├── message     : String   (what went wrong)
            ├── cause       : Throwable (what triggered this — exception chaining)
            ├── stackTrace  : StackTraceElement[] (call chain at creation time)
            └── suppressed  : Throwable[] (exceptions suppressed by try-with-resources)

KEY METHODS ON Throwable:
  getMessage()          returns the message string
  getCause()            returns the causing Throwable (or null)
  getStackTrace()       returns the stack trace array
  printStackTrace()     prints the full chain to stderr
  getSuppressed()       returns suppressed exceptions from try-with-resources
  initCause(Throwable)  sets the cause after construction (older pattern)

Throwable has two direct subclasses:

  java.lang.Error               java.lang.Exception
  JVM and system failures       Application-level failures
  Almost never catch            Always handle with a strategy

The full tree below shows where every commonly encountered type sits.

java.lang.Throwable
    |
    +── java.lang.Error                        JVM-LEVEL (do NOT catch)
    |       ├── VirtualMachineError
    |       │     ├── OutOfMemoryError
    |       │     └── StackOverflowError
    |       ├── LinkageError
    |       │     ├── NoClassDefFoundError
    |       │     └── ExceptionInInitializerError
    |       ├── ThreadDeath
    |       └── AssertionError
    |
    +── java.lang.Exception                    APPLICATION-LEVEL
            |
            +── java.lang.RuntimeException     UNCHECKED — compiler does NOT enforce
            |       ├── NullPointerException
            |       ├── IllegalArgumentException
            |       │     └── NumberFormatException
            |       ├── IllegalStateException
            |       ├── IndexOutOfBoundsException
            |       │     ├── ArrayIndexOutOfBoundsException
            |       │     └── StringIndexOutOfBoundsException
            |       ├── ClassCastException
            |       ├── ArithmeticException
            |       ├── UnsupportedOperationException
            |       └── ConcurrentModificationException
            |
            +── java.io.IOException            CHECKED — compiler enforces handling
            |       ├── FileNotFoundException
            |       └── SocketException
            |
            +── java.sql.SQLException          CHECKED
            +── java.lang.ClassNotFoundException   CHECKED
            +── java.lang.CloneNotSupportedException CHECKED
            +── java.lang.InterruptedException     CHECKED
            +── java.lang.ReflectiveOperationException CHECKED
                    └── NoSuchMethodException

Basic Overview — What Position in the Hierarchy Controls

WHERE A CLASS SITS IN THE HIERARCHY DETERMINES:

  1. WHETHER THE COMPILER ENFORCES HANDLING:
     Subclass of RuntimeException OR Error → unchecked → no compile error if not caught
     Subclass of Exception (not RuntimeException) → checked → must catch or declare throws

  2. WHICH CATCH BLOCK MATCHES:
     Java uses instanceof to match exceptions to catch blocks.
     catch(IOException e) catches IOException AND all its subclasses.
     catch(Exception e) catches EVERYTHING below Exception.
     catch(Throwable t) catches absolutely everything — almost never do this.

  3. CATCH BLOCK ORDER RULE:
     More specific (deeper in tree) MUST appear before more general.
     FileNotFoundException before IOException — FileNotFoundException IS-A IOException.
     IOException before Exception.
     If you put IOException after Exception, the compiler rejects the code
     as "exception IOException has already been caught".

  4. WHAT IT SIGNALS TO CALLERS:
     Checked exception in throws clause = "this failure is expected, handle it"
     Unchecked exception thrown = "this is a programming mistake, fix it"
     Error = "the JVM is in trouble, nothing you can do"

  5. HOW EXCEPTION CHAINING WORKS:
     Every Throwable can hold a cause reference (another Throwable).
     new ServiceException("payment failed", originalSQLException)
     Calling getCause() on the ServiceException returns the SQLExcption.
     printStackTrace() shows the full chain — root cause last.

The Throwable Foundation

Every exception carries four pieces of state inherited from Throwable. Understanding these explains what you see in production logs and how to design useful custom exceptions.

1// File: ThrowableStateDemo.java 2 3public class ThrowableStateDemo { 4 5 public static void main(String[] args) { 6 7 // Constructing and inspecting Throwable state 8 RuntimeException rootCause = new RuntimeException("Database connection refused"); 9 IllegalStateException wrapper = new IllegalStateException( 10 "Service unavailable — downstream DB failed", rootCause); 11 12 System.out.println("=== Throwable state fields ==="); 13 System.out.println("getMessage() : " + wrapper.getMessage()); 14 System.out.println("getCause() : " + wrapper.getCause()); 15 System.out.println("getCause().getMessage(): " + wrapper.getCause().getMessage()); 16 17 System.out.println(); 18 19 // Stack trace elements 20 System.out.println("=== Stack trace elements (top 3) ==="); 21 StackTraceElement[] trace = wrapper.getStackTrace(); 22 for (int i = 0; i < Math.min(3, trace.length); i++) { 23 System.out.println(" [" + i + "] " + trace[i]); 24 } 25 26 System.out.println(); 27 28 // Exception chaining — following the cause chain manually 29 System.out.println("=== Exception cause chain ==="); 30 Throwable current = wrapper; 31 int level = 0; 32 while (current != null) { 33 System.out.printf(" Level %d: %s — %s%n", 34 level++, 35 current.getClass().getSimpleName(), 36 current.getMessage()); 37 current = current.getCause(); 38 } 39 40 System.out.println(); 41 42 // instanceof — the mechanism behind catch block matching 43 System.out.println("=== instanceof relationships (how catch matching works) ==="); 44 IOException ioException = new FileNotFoundException("config.yml not found"); 45 46 System.out.println("FileNotFoundException instanceof FileNotFoundException : " + 47 (ioException instanceof FileNotFoundException)); 48 System.out.println("FileNotFoundException instanceof IOException : " + 49 (ioException instanceof IOException)); 50 System.out.println("FileNotFoundException instanceof Exception : " + 51 (ioException instanceof Exception)); 52 System.out.println("FileNotFoundException instanceof Throwable : " + 53 (ioException instanceof Throwable)); 54 System.out.println("FileNotFoundException instanceof RuntimeException : " + 55 (ioException instanceof RuntimeException)); 56 } 57}
Output:
=== Throwable state fields ===
getMessage()       : Service unavailable — downstream DB failed
getCause()         : java.lang.RuntimeException: Database connection refused
getCause().getMessage(): Database connection refused

=== Stack trace elements (top 3) ===
  [0] ThrowableStateDemo.main(ThrowableStateDemo.java:8)
  [1] ...
  [2] ...

=== Exception cause chain ===
  Level 0: IllegalStateException — Service unavailable — downstream DB failed
  Level 1: RuntimeException — Database connection refused

=== instanceof relationships (how catch matching works) ===
FileNotFoundException instanceof FileNotFoundException : true
FileNotFoundException instanceof IOException           : true
FileNotFoundException instanceof Exception             : true
FileNotFoundException instanceof Throwable            : true
FileNotFoundException instanceof RuntimeException     : false

Checked Exceptions in the Hierarchy

Checked exceptions sit directly under Exception — not under RuntimeException. The compiler uses this position to enforce that callers either handle them or declare them. This position signals a design contract: these are failures that occur in normal operation and that calling code is expected to have a response for.

1// File: CheckedHierarchyDemo.java 2 3import java.io.FileNotFoundException; 4import java.io.IOException; 5import java.sql.SQLException; 6 7public class CheckedHierarchyDemo { 8 9 // The hierarchy determines what each catch block intercepts: 10 // FileNotFoundException is caught by both catch(FileNotFoundException) 11 // AND catch(IOException) — because FileNotFoundException extends IOException 12 static void demonstrateCatchWidening(String path) { 13 try { 14 if (path.endsWith(".missing")) { 15 throw new FileNotFoundException("File not found: " + path); 16 } 17 throw new IOException("Generic I/O failure on: " + path); 18 } catch (FileNotFoundException fnfe) { 19 // Catches only FileNotFoundException (and any subclasses it has) 20 System.out.println("Specific handler — file missing: " + fnfe.getMessage()); 21 } catch (IOException ioe) { 22 // Catches all remaining IOException subclasses not already caught above 23 System.out.println("General I/O handler: " + ioe.getMessage()); 24 } 25 } 26 27 // Checked exceptions require throws declaration if not caught here 28 static String loadConfig(String key) throws IOException, SQLException { 29 if (key == null) throw new IOException("Config key required"); 30 if (key.startsWith("db.")) throw new SQLException("DB lookup not implemented"); 31 return "value-" + key; 32 } 33 34 // Multi-catch: two unrelated checked exceptions handled identically 35 static void processConfigKey(String key) { 36 try { 37 String value = loadConfig(key); 38 System.out.println(key + " = " + value); 39 } catch (IOException | SQLException combinedException) { 40 // Multi-catch: both types handled the same way — variable is final here 41 System.out.println("Config retrieval failed [" + 42 combinedException.getClass().getSimpleName() + "]: " + 43 combinedException.getMessage()); 44 } 45 } 46 47 public static void main(String[] args) { 48 49 System.out.println("=== Catch block widening via hierarchy ==="); 50 demonstrateCatchWidening("report.missing"); 51 demonstrateCatchWidening("report.csv"); 52 53 System.out.println(); 54 55 System.out.println("=== Multi-catch with unrelated checked exceptions ==="); 56 processConfigKey("app.name"); 57 processConfigKey(null); 58 processConfigKey("db.host"); 59 } 60}
Output:
=== Catch block widening via hierarchy ===
Specific handler — file missing: File not found: report.missing
General I/O handler: Generic I/O failure on: report.csv

=== Multi-catch with unrelated checked exceptions ===
app.name = value-app.name
Config retrieval failed [IOException]: Config key required
Config retrieval failed [SQLException]: DB lookup not implemented

RuntimeException in the Hierarchy

RuntimeException sits between Exception and all unchecked exception types. Its position in the tree is what makes the whole checked/unchecked distinction work — the compiler looks at whether an exception's class is a subtype of RuntimeException to decide whether to enforce handling.

1// File: RuntimeHierarchyDemo.java 2 3import java.util.List; 4import java.util.Map; 5 6public class RuntimeHierarchyDemo { 7 8 record Product(String id, String name, double price) {} 9 10 // IllegalArgumentException: subtype of RuntimeException — unchecked 11 // Used for contract violations — callers who pass bad arguments should fix their code 12 static Product validateProduct(String id, String name, double price) { 13 if (id == null || id.isBlank()) { 14 throw new IllegalArgumentException("Product ID must not be blank"); 15 } 16 if (price < 0) { 17 throw new IllegalArgumentException("Price cannot be negative: " + price); 18 } 19 return new Product(id, name, price); 20 } 21 22 // IllegalStateException: subtype of RuntimeException — unchecked 23 // Used when the object is in the wrong state for the requested operation 24 static class OrderProcessor { 25 private boolean initialized = false; 26 private String currentOrderId = null; 27 28 void initialize() { this.initialized = true; } 29 30 void startOrder(String orderId) { 31 if (!initialized) { 32 // Calling startOrder before initialize() is an object lifecycle error 33 throw new IllegalStateException( 34 "OrderProcessor must be initialized before starting an order"); 35 } 36 this.currentOrderId = orderId; 37 } 38 39 String getCurrentOrderId() { 40 if (currentOrderId == null) { 41 throw new IllegalStateException("No active order — call startOrder() first"); 42 } 43 return currentOrderId; 44 } 45 } 46 47 // UnsupportedOperationException: subtype of RuntimeException 48 // Thrown by Collections.unmodifiableList and similar read-only views 49 static void demonstrateUnsupportedOp() { 50 List<String> readOnly = List.of("spring", "kafka", "redis"); 51 try { 52 readOnly.add("docker"); // List.of() does not support mutation 53 } catch (UnsupportedOperationException uoe) { 54 System.out.println("Cannot modify immutable list: " + uoe.getClass().getSimpleName()); 55 } 56 } 57 58 public static void main(String[] args) { 59 60 System.out.println("=== IllegalArgumentException (contract violation) ==="); 61 try { 62 validateProduct(null, "Laptop", 45000); 63 } catch (IllegalArgumentException iae) { 64 System.out.println("Caught: " + iae.getMessage()); 65 } 66 Product valid = validateProduct("P001", "Laptop", 45000); 67 System.out.println("Valid product created: " + valid); 68 69 System.out.println(); 70 71 System.out.println("=== IllegalStateException (lifecycle violation) ==="); 72 OrderProcessor processor = new OrderProcessor(); 73 try { 74 processor.startOrder("ORD-9821"); // not initialized yet 75 } catch (IllegalStateException ise) { 76 System.out.println("Caught: " + ise.getMessage()); 77 } 78 processor.initialize(); 79 processor.startOrder("ORD-9821"); 80 System.out.println("Active order: " + processor.getCurrentOrderId()); 81 82 System.out.println(); 83 84 System.out.println("=== UnsupportedOperationException (read-only view) ==="); 85 demonstrateUnsupportedOp(); 86 } 87}
Output:
=== IllegalArgumentException (contract violation) ===
Caught: Product ID must not be blank
Valid product created: Product[id=P001, name=Laptop, price=45000.0]

=== IllegalStateException (lifecycle violation) ===
Caught: OrderProcessor must be initialized before starting an order
Active order: ORD-9821

=== UnsupportedOperationException (read-only view) ===
Cannot modify immutable list: UnsupportedOperationException

How Exception Matching Works Internally

The JVM uses instanceof checks to match thrown exceptions against catch blocks. It evaluates each catch clause from top to bottom and executes the first block whose declared type is a supertype of (or the same type as) the thrown exception.

CATCH BLOCK MATCHING — JVM evaluation order:

  thrown: FileNotFoundException (extends IOException extends Exception)

  try block
    catch (NullPointerException e) → FileNotFoundException instanceof NPE? NO
    catch (FileNotFoundException e) → FileNotFoundException instanceof FNFE? YES → EXECUTE
    catch (IOException e)           → never reached
    catch (Exception e)             → never reached

  thrown: SocketException (extends IOException extends Exception)

  try block
    catch (NullPointerException e) → SocketException instanceof NPE? NO
    catch (FileNotFoundException e) → SocketException instanceof FNFE? NO
    catch (IOException e)           → SocketException instanceof IOException? YES → EXECUTE
    catch (Exception e)             → never reached

COMPILER ENFORCEMENT OF ORDER:
  The compiler rejects this:
    catch (IOException e)         ← general
    catch (FileNotFoundException) ← specific — already caught by IOException above
  Error: "exception FileNotFoundException has already been caught"

  Correct order — specific before general:
    catch (FileNotFoundException) ← specific first
    catch (IOException e)         ← general after

Custom Exceptions and Their Position

Where you place your custom exception class in the hierarchy is a design decision that determines what callers must do with it. This choice is one of the most important parts of designing a service or library API.

1// File: CustomExceptionHierarchyDemo.java 2 3// CHECKED: callers must handle or declare 4// Use when: the failure is expected, the caller has a recovery strategy 5class InsufficientStockException extends Exception { 6 7 private final String productId; 8 private final int requested; 9 private final int available; 10 11 InsufficientStockException(String productId, int requested, int available) { 12 super(String.format("Insufficient stock for %s: requested=%d available=%d", 13 productId, requested, available)); 14 this.productId = productId; 15 this.requested = requested; 16 this.available = available; 17 } 18 19 String getProductId() { return productId; } 20 int getRequested() { return requested; } 21 int getAvailable() { return available; } 22} 23 24// UNCHECKED: callers do not have to handle it 25// Use when: the failure indicates a programming error or system-level problem 26class CatalogConfigurationException extends RuntimeException { 27 28 CatalogConfigurationException(String message) { 29 super(message); 30 } 31 32 CatalogConfigurationException(String message, Throwable cause) { 33 super(message, cause); // always pass cause when wrapping 34 } 35} 36 37// UNCHECKED: wraps a lower-level failure to add domain context 38// Use when: the original exception is too technical for the calling layer 39class InventoryServiceException extends RuntimeException { 40 41 private final String operationType; 42 43 InventoryServiceException(String operationType, String message, Throwable cause) { 44 super("[" + operationType + "] " + message, cause); 45 this.operationType = operationType; 46 } 47 48 String getOperationType() { return operationType; } 49}
1// File: InventoryService.java 2 3public class InventoryService { 4 5 private final java.util.Map<String, Integer> stock = new java.util.HashMap<>(); 6 7 public InventoryService() { 8 stock.put("P001", 10); 9 stock.put("P002", 0); 10 // P003 intentionally missing — simulate misconfigured product 11 } 12 13 // Throws checked InsufficientStockException — caller decides what to do when stock is low 14 public void reserveStock(String productId, int quantity) 15 throws InsufficientStockException { 16 17 if (!stock.containsKey(productId)) { 18 // Unchecked — this is a configuration mistake, not a runtime business condition 19 throw new CatalogConfigurationException( 20 "Product not found in inventory catalog: " + productId); 21 } 22 23 int available = stock.get(productId); 24 if (available < quantity) { 25 // Checked — caller should handle "not enough stock" as a business scenario 26 throw new InsufficientStockException(productId, quantity, available); 27 } 28 29 stock.put(productId, available - quantity); 30 System.out.printf("Reserved %d units of %s. Remaining: %d%n", 31 quantity, productId, stock.get(productId)); 32 } 33 34 public static void main(String[] args) { 35 36 InventoryService service = new InventoryService(); 37 38 System.out.println("=== Successful reservation ==="); 39 try { 40 service.reserveStock("P001", 3); 41 } catch (InsufficientStockException ise) { 42 System.out.println("Stock issue: " + ise.getMessage()); 43 } 44 45 System.out.println(); 46 47 System.out.println("=== Checked exception — business condition, caller decides ==="); 48 try { 49 service.reserveStock("P002", 5); // 0 available 50 } catch (InsufficientStockException ise) { 51 System.out.printf("Out of stock — product=%s requested=%d available=%d%n", 52 ise.getProductId(), ise.getRequested(), ise.getAvailable()); 53 System.out.println("Action: adding to waitlist"); 54 } 55 56 System.out.println(); 57 58 System.out.println("=== Unchecked exception — configuration error, propagates ==="); 59 try { 60 service.reserveStock("P003", 2); // not in catalog 61 } catch (CatalogConfigurationException cce) { 62 // Caught here for demo — in production this would propagate to a global handler 63 System.out.println("System error [" + cce.getClass().getSimpleName() + "]: " + 64 cce.getMessage()); 65 } 66 } 67}
Output:
=== Successful reservation ===
Reserved 3 units of P001. Remaining: 7

=== Checked exception — business condition, caller decides ===
Out of stock — product=P002 requested=5 available=0
Action: adding to waitlist

=== Unchecked exception — configuration error, propagates ===
System error [CatalogConfigurationException]: Product not found in inventory catalog: P003

Real-World Example — Razorpay Payment Gateway Layer

A payment gateway layer at Razorpay handles multiple failure modes across different architectural layers. Each exception class is placed precisely in the hierarchy: checked exceptions for expected business failures that callers must plan for, unchecked exceptions for system conditions and programming errors that propagate to a global handler.

1// File: GatewayException.java 2 3// Base class for the entire gateway exception family 4// Unchecked — payment failures propagate to a global API error handler 5public class GatewayException extends RuntimeException { 6 7 private final String errorCode; 8 private final int httpStatus; 9 10 public GatewayException(String errorCode, int httpStatus, String message) { 11 super(message); 12 this.errorCode = errorCode; 13 this.httpStatus = httpStatus; 14 } 15 16 public GatewayException( 17 String errorCode, int httpStatus, String message, Throwable cause) { 18 super(message, cause); 19 this.errorCode = errorCode; 20 this.httpStatus = httpStatus; 21 } 22 23 public String getErrorCode() { return errorCode; } 24 public int getHttpStatus() { return httpStatus; } 25}
1// File: PaymentDeclinedException.java 2 3// Specific subclass — card declined by the issuing bank 4// More specific than GatewayException; catch blocks can target this precisely 5public class PaymentDeclinedException extends GatewayException { 6 7 private final String declineReason; 8 9 public PaymentDeclinedException(String declineReason, String orderId) { 10 super("PAYMENT_DECLINED", 402, 11 "Payment declined for order " + orderId + ": " + declineReason); 12 this.declineReason = declineReason; 13 } 14 15 public String getDeclineReason() { return declineReason; } 16}
1// File: GatewayTimeoutException.java 2 3// Another specific subclass — downstream bank/network timeout 4public class GatewayTimeoutException extends GatewayException { 5 6 private final String gatewayName; 7 private final int timeoutMillis; 8 9 public GatewayTimeoutException(String gatewayName, int timeoutMillis, Throwable cause) { 10 super("GATEWAY_TIMEOUT", 504, 11 gatewayName + " did not respond within " + timeoutMillis + "ms", cause); 12 this.gatewayName = gatewayName; 13 this.timeoutMillis = timeoutMillis; 14 } 15 16 public String getGatewayName() { return gatewayName; } 17 public int getTimeoutMillis() { return timeoutMillis; } 18}
1// File: PaymentController.java 2 3public class PaymentController { 4 5 // The hierarchy enables precise catch block targeting: 6 // PaymentDeclinedException before GatewayException (subtype before supertype) 7 public void processPayment(String orderId, double amount, String cardType) { 8 9 System.out.printf("Processing: order=%s amount=Rs.%.0f card=%s%n", 10 orderId, amount, cardType); 11 12 try { 13 executePayment(orderId, amount, cardType); 14 System.out.println(" HTTP 200 — Payment confirmed"); 15 16 } catch (PaymentDeclinedException declined) { 17 // Most specific subclass — caught first 18 System.out.printf(" HTTP 402 [%s] — %s%n", 19 declined.getErrorCode(), declined.getMessage()); 20 21 } catch (GatewayTimeoutException timeout) { 22 // Another specific subclass — caught before the general GatewayException 23 System.out.printf(" HTTP 504 [%s] — %s (retry eligible)%n", 24 timeout.getErrorCode(), timeout.getMessage()); 25 26 } catch (GatewayException gateway) { 27 // General gateway exception — catches anything not already caught above 28 System.out.printf(" HTTP %d [%s] — %s%n", 29 gateway.getHttpStatus(), gateway.getErrorCode(), gateway.getMessage()); 30 31 } catch (Exception unexpected) { 32 // Absolute last resort — only for truly unexpected non-gateway failures 33 System.out.printf(" HTTP 500 — Unexpected: %s%n", 34 unexpected.getClass().getSimpleName()); 35 } 36 } 37 38 private void executePayment(String orderId, double amount, String cardType) { 39 if ("DECLINED-CARD".equals(cardType)) { 40 throw new PaymentDeclinedException("Insufficient funds", orderId); 41 } 42 if ("TIMEOUT-CARD".equals(cardType)) { 43 throw new GatewayTimeoutException("HDFC-GATEWAY", 5000, 44 new RuntimeException("Connection timed out")); 45 } 46 if (amount > 200_000) { 47 throw new GatewayException("LIMIT_EXCEEDED", 422, 48 "Transaction amount exceeds per-transaction limit"); 49 } 50 } 51 52 public static void main(String[] args) { 53 PaymentController controller = new PaymentController(); 54 55 controller.processPayment("ORD-001", 2499.0, "VISA"); 56 System.out.println(); 57 controller.processPayment("ORD-002", 5000.0, "DECLINED-CARD"); 58 System.out.println(); 59 controller.processPayment("ORD-003", 1299.0, "TIMEOUT-CARD"); 60 System.out.println(); 61 controller.processPayment("ORD-004", 250_000.0, "AMEX"); 62 } 63}
Output:
Processing: order=ORD-001 amount=Rs.2499 card=VISA
  HTTP 200 — Payment confirmed

Processing: order=ORD-002 amount=Rs.5000 card=DECLINED-CARD
  HTTP 402 [PAYMENT_DECLINED] — Payment declined for order ORD-002: Insufficient funds

Processing: order=ORD-003 amount=Rs.1299 card=TIMEOUT-CARD
  HTTP 504 [GATEWAY_TIMEOUT] — HDFC-GATEWAY did not respond within 5000ms (retry eligible)

Processing: order=ORD-004 amount=Rs.250000 card=AMEX
  HTTP 422 [LIMIT_EXCEEDED] — Transaction amount exceeds per-transaction limit

Performance Considerations

All exception construction involves one expensive operation: capturing the stack trace. This happens at the moment the new SomeException(...) object is created — not when it is thrown, not when it is caught, not when printStackTrace() is called. The stack trace is a snapshot of all active method frames on the calling thread at creation time.

EXCEPTION CONSTRUCTION COST:

  new RuntimeException("message")
    → JVM walks the current call stack
    → Creates a StackTraceElement for each frame
    → 200-frame stack → 200 objects allocated
    → Expensive: microseconds, not nanoseconds

  Implications:
    Creating exceptions in tight loops degrades performance significantly.
    "Exceptional" means rare — not "every iteration that finds no result".
    For control flow (not-found, no-data), return Optional or null — not exceptions.

  SUPPRESSING STACK TRACE (valid for control-flow use cases in frameworks):
    new RuntimeException("message") {
        @Override
        public synchronized Throwable fillInStackTrace() {
            return this; // skips stack capture — exception creation becomes cheap
        }
    };
    Only use this in frameworks where exception objects serve as signals, not diagnostics.

Best Practices

Place custom exceptions at exactly the right level in the hierarchy. Extending Exception (checked) signals "this is a recoverable business condition — callers must have a plan." Extending RuntimeException (unchecked) signals "this is a system problem or programming error — handle at the boundary, not everywhere." A single application typically needs both, and mixing them — making a routine business failure unchecked, or making a programming error checked — pollutes every caller with unnecessary catch blocks or silently swallows errors.

Design exception hierarchies with a shared base class for each subsystem. A payment service should have a PaymentException base, under which PaymentDeclinedException, GatewayTimeoutException, and FraudDetectionException sit. This lets callers catch the base class when they only care that "something payment-related failed," and target specific subclasses when they need different handling per failure mode. This is the same pattern the JDK itself uses: IOException as a base for all java.io failures.

Always add a cause when wrapping exceptions. throw new ServiceException("payment failed", originalSqlException) preserves the full diagnostic chain. Without passing the cause, getCause() returns null and the root cause disappears from logs. Production debugging without the cause chain is dramatically harder — this is the single most impactful exception-handling rule in real codebases.

Keep the exception hierarchy shallow. Three levels below Exception is almost always enough: a subsystem base, a category (business vs system), and specific types. Hierarchies deeper than that require developers to memorise too much to use the catch blocks correctly. A hierarchy nobody can remember is not used correctly.

Common Mistakes

Mistake 1 — Putting a Specific Catch Block After a General One

1// WRONG — compiler rejects this: FileNotFoundException already caught by IOException 2try { 3 openFile("config.yml"); 4} catch (IOException ioe) { 5 System.out.println("IO error"); 6} catch (FileNotFoundException fnfe) { 7 // This line never compiles — compiler flags it as unreachable 8 System.out.println("File missing"); 9} 10 11// CORRECT — specific (deeper in tree) always goes first 12try { 13 openFile("config.yml"); 14} catch (FileNotFoundException fnfe) { 15 System.out.println("File missing: " + fnfe.getMessage()); 16} catch (IOException ioe) { 17 System.out.println("Other IO error: " + ioe.getMessage()); 18}

Mistake 2 — Making Domain Exceptions Extend Exception When They Should Extend RuntimeException

1// WRONG — OrderNotFoundException is a checked exception 2// Every service method that might not find an order now requires 3// throws OrderNotFoundException in its signature, polluting the entire call chain 4class OrderNotFoundException extends Exception { // checked — forces every caller to declare 5 OrderNotFoundException(String orderId) { 6 super("Order not found: " + orderId); 7 } 8} 9 10// This ripples up: 11public Order getOrder(String orderId) throws OrderNotFoundException { ... } 12public Invoice generateInvoice(String orderId) throws OrderNotFoundException { ... } 13public void sendConfirmation(String orderId) throws OrderNotFoundException { ... } 14// Every method that uses orderId now carries the checked exception declaration 15 16// CORRECT — for a "not found" that callers handle at the controller layer: 17class OrderNotFoundException extends RuntimeException { // unchecked 18 OrderNotFoundException(String orderId) { 19 super("Order not found: " + orderId); 20 } 21} 22// Now only the controller catches it — business logic methods stay clean

Mistake 3 — Losing the Cause When Wrapping

1// WRONG — original SQLException is swallowed 2// getCause() returns null, root cause never appears in logs 3public void saveOrder(Order order) { 4 try { 5 repository.save(order); 6 } catch (java.sql.SQLException sqlEx) { 7 throw new InventoryServiceException("SAVE", "Failed to save order", null); 8 // cause is null ^^^ 9 } 10} 11 12// CORRECT — always pass the original exception as the second argument 13public void saveOrder(Order order) { 14 try { 15 repository.save(order); 16 } catch (java.sql.SQLException sqlEx) { 17 throw new InventoryServiceException("SAVE", "Failed to save order", sqlEx); 18 // Full chain in logs: InventoryServiceException → SQLException → DB error 19 } 20}

Mistake 4 — Creating Redundant Exception Classes That Add No Information

1// WRONG — just wraps RuntimeException with no additional state or meaning 2class GeneralException extends RuntimeException { 3 GeneralException(String message) { super(message); } 4} 5 6// This adds nothing over throwing RuntimeException directly. 7// Exception classes earn their existence by carrying domain-specific fields: 8// orderId, errorCode, httpStatus, customerId — something the message string alone cannot. 9 10// CORRECT — every custom exception class should have at least one domain-specific field 11class OrderProcessingException extends RuntimeException { 12 private final String orderId; 13 private final String failureStage; // "VALIDATION", "PAYMENT", "FULFILMENT" 14 15 OrderProcessingException(String orderId, String failureStage, String message, Throwable cause) { 16 super(message, cause); 17 this.orderId = orderId; 18 this.failureStage = failureStage; 19 } 20 21 String getOrderId() { return orderId; } 22 String getFailureStage() { return failureStage; } 23}

Interview Questions

Q1. What is the root of the Java exception hierarchy and what does it provide?

java.lang.Throwable is the root. Every class that can be thrown or caught in Java extends it directly or indirectly. Throwable provides four core fields: a String message describing the failure, a Throwable cause for exception chaining, a StackTraceElement[] array captured at construction time, and a Throwable[] array of suppressed exceptions added by try-with-resources. The key methods are getMessage(), getCause(), getStackTrace(), printStackTrace(), and getSuppressed().

Q2. How does the JVM decide which catch block handles a thrown exception?

The JVM evaluates catch clauses from top to bottom using instanceof checks. The first catch block whose declared type is the same as or a supertype of the thrown exception's class is selected for execution. This is why catch blocks must be ordered from most specific to most general — a catch (IOException e) before catch (FileNotFoundException e) makes the FileNotFoundException block unreachable, because FileNotFoundException is-a IOException and the first block would match. The compiler detects and rejects this ordering.

Q3. What is the difference between checked and unchecked exceptions in terms of their position in the hierarchy?

The position in the hierarchy is the mechanism: any class that extends Exception without going through RuntimeException is a checked exception. Any class that extends RuntimeException (which extends Exception) is unchecked. The compiler uses this structural property to enforce handling — if a method throws a checked exception, every calling method must either surround the call with try-catch or declare throws in its own signature. RuntimeException and Error subclasses are exempt from this enforcement.

Q4. If you create a custom exception, should it extend Exception or RuntimeException, and why?

The decision is about recoverability and caller responsibility. Extend Exception (checked) when the failure represents an expected, recoverable business condition that callers should explicitly plan for — InsufficientStockException, PaymentDeclinedException. Extend RuntimeException (unchecked) when the failure represents a programming error, configuration problem, or system failure that callers should not need to handle case-by-case — CatalogConfigurationException, InvalidArgumentException. Choosing wrongly pollutes APIs: checked exceptions for routine conditions force every intermediate method to carry the throws declaration; unchecked exceptions for serious business failures hide conditions callers need to handle.

Q5. Why must catch blocks be ordered from most specific to most general?

Because the JVM evaluates catch blocks top-to-bottom using instanceof matching, and instanceof returns true for all supertypes. If catch (Exception e) appears before catch (IOException e), every IOException matches the first block because IOException is-a Exception. The IOException block never executes. The Java compiler detects this as unreachable code and treats it as a compile error: "exception IOException has already been caught." The ordering rule is enforced at compile time, not runtime.

Q6. What is exception chaining and why does the cause matter in production?

Exception chaining is the practice of wrapping one exception inside another by passing the original as the cause argument to the new exception's constructor. new ServiceException("failed", originalSqlException) stores the SQLException as the cause. getCause() returns it. printStackTrace() prints the entire chain. In production, the root cause is what actually went wrong — "failed to save order" is not actionable; "ORA-00942: table or view does not exist" tells you exactly what to fix. Wrapping exceptions without preserving the cause destroys diagnostic information and makes debugging dramatically harder.

Q7. How does the exception hierarchy enable a global exception handler pattern?

A global exception handler (like a Spring Boot @ControllerAdvice, or a Thread.UncaughtExceptionHandler) catches exception types and maps them to responses. Because every domain exception extends a common base — say GatewayException — the global handler can catch (GatewayException e) and map e.getHttpStatus() to the HTTP response code. More specific subclasses like PaymentDeclinedException and GatewayTimeoutException can be caught individually for specific mappings, while the base class catches any unrecognised subtype. This hierarchy-aware pattern is exactly how Spring Boot's exception handling works internally.

FAQs

What is java.lang.Throwable and why do we rarely use it directly?

Throwable is the root class of the exception hierarchy — the only type that can appear after throw or in a catch clause. We rarely use it directly because catching Throwable swallows both Exception (application failures) and Error (JVM failures) through one handler. This makes it impossible to distinguish recoverable failures from catastrophic ones. Framework code uses Throwable in thread pool workers and test frameworks where intercepting everything is intentional. In business logic, it signals unaddressed design problems.

Why does NumberFormatException extend IllegalArgumentException instead of Exception directly?

NumberFormatException is thrown when a string passed to a number-parsing method has invalid format — a contract violation by the caller. It shares its cause and handling strategy with IllegalArgumentException (bad arguments violate method contracts). Placing it under IllegalArgumentException means that a catch (IllegalArgumentException e) block already handles both, and code that specifically cares about number format issues can target NumberFormatException more precisely. This is the hierarchy working as designed: related failures cluster together.

What is the difference between Exception and RuntimeException?

RuntimeException extends Exception. The only structural difference is that the Java compiler treats RuntimeException and all its subclasses as unchecked — it does not require callers to handle or declare them. Exception subclasses that do not go through RuntimeException are checked — the compiler enforces handling. In terms of code behaviour at runtime, there is no difference: both are caught by catch (Exception e), both carry messages and causes, and both produce stack traces.

Is it possible to create a hierarchy of custom exceptions?

Yes, and it is the recommended pattern for large codebases. Define a module-level base exception (PaymentException extends RuntimeException), then specific subclasses below it (PaymentDeclinedException extends PaymentException). Callers that handle any payment failure use catch (PaymentException e). Callers that differentiate between specific failures use catch (PaymentDeclinedException e) first. This mirrors the JDK design: IOException as a base, FileNotFoundException and SocketException as specific subtypes.

Can I catch multiple exception types that are unrelated in the hierarchy?

Yes — multi-catch syntax handles this: catch (IOException | SQLException exception). The two types do not need to share a common ancestor below Throwable (other than Throwable itself). The variable in a multi-catch block is implicitly final — you cannot reassign it. Multi-catch is appropriate when two different exception types genuinely require identical handling and you want to avoid duplicating the catch body.

Does catching a supertype affect the stack trace or exception object?

No. Catching IOException instead of FileNotFoundException does not modify the exception object in any way. The exception is the same object — same class, same message, same cause, same stack trace — regardless of which catch clause intercepts it. The catch clause only determines which block of code runs; it does not alter the exception itself.

Summary

The Java exception hierarchy is a class tree rooted at Throwable, branching into Error (JVM failures) and Exception (application failures), with RuntimeException as the dividing line between checked and unchecked within the Exception branch. Every decision that seems like a Java syntax rule — catch ordering, throws declarations, compiler errors about unhandled exceptions — is a direct consequence of this tree structure combined with instanceof matching.

The hierarchy serves three practical purposes: it defines what the compiler requires, it determines which catch block fires, and it communicates intent to callers. A well-designed exception hierarchy for a module has a base class, specific subclasses with domain-relevant fields, checked exceptions for recoverable business conditions, unchecked exceptions for programming errors and system failures, and a cause preserved on every wrapped exception.

For interviews, the questions about hierarchy almost always test whether you can explain checked versus unchecked in terms of tree position, articulate why catch ordering is specific-before-general, and describe how exception chaining works through the cause reference. These three questions together cover what most companies actually care about.

What to Read Next