Why is Exception Handling Important? #


Exceptions Happen Regularly

  • Why? Complex code, changing environments, and human errors
    • Defects Are Inevitable – Even great developers miss edge cases
    • Runtime Surprises – Files missing, servers down, data corrupted
    • External Systems Fail – APIs, databases, and networks aren’t always reliable
    • Environment Changes – OS updates, memory limits, config issues, etc.
  • Moral of the Story? Expect exceptions. Handle them gracefully.

Need to Handle Them Properly

  • What? Handling exceptions isn't just about catching them
    • It's about solving them gracefully without scaring your end users!
  • 1. User Communication
    • Show friendly, non-technical error messages
    • Avoid app crashes or ugly stack traces
    • Display a unique error ID the user can report
    • Guide users on how to contact support
  • 2. Debugging Support
    • Log the full stack trace and context
    • Include the same error ID shown to the user
    • Capture input data, user actions, or environment details
    • Helps support/dev teams trace and fix the issue fast

Which Design Pattern Powers Java's Exception Flow? #


  • Chain of Responsibility: A behavioral pattern where a request moves through a chain of handlers
  • Why?: Decouples sender and receiver — each handler decides to process or pass along
  • How It Works:
    • Exception travels up the call stack method by method
    • Each method can catch or pass the exception higher
    • Stops when an appropriate handler is found
  • Real-World Analogy:
    • Like a leave request going from employee → manager → HR
    • Each can approve or escalate the request
    public static void main(String[] args) {
        method1();
    }
    private static void method1() {
        method2();
    }
    private static void method2() {
        String str = null;
        str.toString();  // Throws NullPointerException
    }
    
    //Exception in thread "main" 
    //java.lang.NullPointerException at
    //method2(ExceptionHandlingExample1.java:15)
    //at method1(ExceptionHandlingExample1.java:10)
    //at main(ExceptionHandlingExample1.java:6)

Can You Walk Through a Practical Example of Java Exception Handling? #


  • PaymentApp Tries to make a payment of $600 (more than balance) using PaymentService
    • Custom exception InsufficientFundsException extends Exception
    • PaymentService throws InsufficientFundsException if not enough money
    • PaymentApp Uses try-catch-finally to handle exceptions cleanly
      • Logs and prints appropriate error or success messages
      • Always prints “Transaction attempt completed” using finally
public class PaymentApp {
    public static void main(String[] args) {
        PaymentService paymentService = new PaymentService();

        try {
            paymentService.makePayment(600);
        } catch (InsufficientFundsException e) {
            System.err.println(
                "Error: " + e.getMessage());
        } catch (IllegalArgumentException e) {
            System.err.println(
                "Invalid Input: " + e.getMessage());
        } catch (Exception e) {
            System.err.println(
                "Unexpected error: " + e.getMessage());
        } finally {
            System.out.println(
                "Transaction attempt completed.");
        }
    }
}

class PaymentService {
    private static final Logger logger 
        = Logger.getLogger(PaymentService.class.getName());

    private double balance = 500.0;  // Initial balance

    public void makePayment(double amount) 
                throws InsufficientFundsException {
        logger.info("Processing payment of $" + amount);

        if (amount <= 0) {
            throw new IllegalArgumentException
                ("Payment amount must be greater than zero.");
        }
        if (amount > balance) {
            throw new InsufficientFundsException
                ("Available balance: $" 
                    + balance);
        }

        balance -= amount;
        System.out.println
            ("Payment of $" + amount 
                + " successful. Remaining balance: $" 
                + balance);
    }
}

class InsufficientFundsException extends Exception {
    public InsufficientFundsException(String message) {
        super(message);
    }
}

1. Try-Catch Block (Handling Exceptions Gracefully)

  • What? Used to catch and handle exceptions to prevent program crashes.
  • Where? In main(), different catch blocks handle specific exceptions.
    try {
        paymentService.makePayment(600); 
    } catch (InsufficientFundsException e) {
        System.err.println("Error: " 
                            + e.getMessage());
    } catch (IllegalArgumentException e) {
        System.err.println("Invalid Input: " 
                            + e.getMessage());
    } catch (Exception e) {
        System.err.println("Unexpected error: " 
                            + e.getMessage());
    }
  • Key Points:
    • Multiple catch Blocks: Each block handles a different exception type.
    • Order of Exceptions -> Specific to General: InsufficientFundsException (custom exception) comes before Exception (general).

2. Custom Exception (InsufficientFundsException)

  • What? A user-defined exception for handling specific errors.
  • Why? Provides meaningful error messages instead of generic exceptions.
  • Extends Exception → Makes it a checked exception.
    class InsufficientFundsException extends Exception {
        public InsufficientFundsException(String message) {
            super(message);
        }
    }

3. Throw and Throws (Propagating Exceptions)

  • What? The throw statement generates exceptions, and throws declares them in the method signature.
  • Where? Inside PaymentService.makePayment()
    public void makePayment(double amount) 
                    throws InsufficientFundsException {
    
        if (amount > balance) {
            throw new InsufficientFundsException
                    ("Available balance: $" + balance);
        }
    
    }
  • Key Points: throw vs throws
    • throw new InsufficientFundsException(...) → Generates the exception.
    • throws InsufficientFundsException → Informs the caller (main()) to handle it.

4. Finally Block (Ensuring Cleanup Code Executes)

  • What? The finally block (almost) always executes whether an exception occurs or not.
  • Why? Used for cleanup tasks like closing connections or logging actions.
    finally {
        // Almost Always executes
        // Clean up!
        System.out.println("Transaction attempt completed."); 
    }
  • Key Points:
    • Runs even if an exception occurs.
    • Used for releasing resources, or performing essential operations.

5. Catch-All Exception Handling (Unexpected Errors)

  • What? The last catch block catches any unexpected exceptions.
  • Why? Ensures the program doesn’t crash due to an unhandled error.
    catch (Exception e) {
        System.err.println(
            "Unexpected error: " + e.getMessage());
    }
  • Key Points:
    • Catches any other runtime exceptions that are not explicitly handled.
    • Helps in debugging unexpected errors.

What Happens When You Swallow Exceptions — And Why Is It Bad? #


  • What? An exception is caught but silently ignored
  • Why It's Bad? Looks like everything is fine... but it's not
  • No Logging = No Clues – Developer's can't trace what went wrong
  • Program Continues – But state might be broken or inconsistent
  • Result? Bugs hide, grow, and jump out later in weirder places
  • ❌ Example of Exception Swallowing (Bad Practice)
    try {
    
        int[] numbers = {1, 2};
        
        // ❌ ArrayIndexOutOfBoundsException
        System.out.println(numbers[3]); 
    
    } catch (Exception e) {
    
        // ❌ Exception is caught but ignored
    
    }
    
    System.out.println("Program continues...");
  • Why Is This a Problem?
    • No Clue Something Broke – Developer can’t fix what they can’t see
    • Silent Failures = Sneaky Bugs – App keeps running in a bad state
    • Delayed Explosions – Errors surface much later in unrelated places
    • Wasted Time – Debugging without logs is like finding a needle in a dark room
    • Always log, handle, or rethrow exceptions
      • Your future self (and your team) will thank you!

In What Scenarios is Code in finally Not Executed? #


  • ❌ Example Without finally (Problem Case)
    private static void method2() {
        Connection connection = new Connection();
        connection.open();
        try {
            // LOGIC
            String str = null;
            str.toString();
        } catch (Exception e) {
            System.out.println("Exception Handled - Method 2");
        }
        // Connection is never closed if exception occurs ❌
    }
    • Problem: If an exception occurs, opened connections may remain unclosed.
  • ✅ Example With finally (Proper Resource Cleanup)
    private static void method2() {
        Connection connection = new Connection();
        connection.open();
        try {
            String str = null;
            str.toString();
        } catch (Exception e) {
            System.out.println("Exception Handled - Method 2");
        } finally {
            // Ensures connection is always closed ✅
            connection.close();  
        }
    }
  • finally is NOT executed in these two cases
    • Exception Inside finally – If something goes wrong inside finally, it may skip the rest
    • JVM Shutdown – If the JVM dies (e.g., System.exit() or crash), finally won’t get a chance

What Combinations of try, catch, and finally Are Allowed? #


  • try + catch – ✅ Allowed
    • Handles exceptions, no need for finally
    try {
        riskyThing();
    } catch (Exception e) {
        handleIt(e);
    }
  • try + finally – ✅ Allowed
    • Cleanup logic runs, even without handling exceptions
    try {
        riskyThing();
    } finally {
        cleanup();
    }
  • try + catch + finally – ✅ Allowed
    • The full combo: handle errors + cleanup
    try {
        riskyThing();
    } catch (Exception e) {
        handleIt(e);
    } finally {
        cleanup();
    }
  • try Alone – ❌ Not allowed
    • Compilation error: must have at least a catch or finally
    try {
        riskyThing();
    }
    // ❌ Error: Missing catch or finally

What Is the Class Hierarchy Behind Java Exceptions? #


Class Name Category (Parent) Checked? Description
Throwable Root of all exceptions and errors
Error Throwable No Serious system-level issues
OutOfMemoryError VirtualMachineError (Error) No JVM ran out of memory
StackOverflowError VirtualMachineError (Error) No Stack memory exhausted (e.g., recursion)
Exception Throwable Yes Base for all application exceptions
IOException Exception Yes Input/output failure
SQLException Exception Yes Database access error
Class NotFound Exception Exception Yes Class loading failed
RuntimeException Exception No Base for unchecked exceptions
NullPointerException RuntimeException No Accessing null reference
ArrayIndex OutOfBounds Exception RuntimeException No Invalid array index
Illegal Argument Exception RuntimeException No Invalid method argument
Arithmetic Exception RuntimeException No Math error (e.g., divide by zero)

Error vs Exception

Error Exception
Used for critical system failures. Used for recoverable issues.
Cannot be handled by the programmer. Can be handled by the programmer.
Examples: StackOverflowError, OutOfMemoryError. Examples: IOException, NullPointerException.

How Do Checked and Unchecked Exceptions Differ in Java? #


1. What Are Checked Exceptions?

  • What? Subclasses of Exception excluding RuntimeException and its subclasses
  • When? Compiler checks them at compile time
  • Must Handle? Yes — use try-catch or declare with throws
  • Why? These represent recoverable problems (like file not found)
  • Common Checked Exceptions:
    • IOException – File or network access error
    • ClassNotFoundException – Class loading issue
  • Compiler Says: "Handle it, or tell me you're throwing it!"
    import java.io.*;
    
    class FileReaderExample {
        public static void readFile() 
            throws IOException {  
            // Method must declare or handle IOException
            
            FileReader file = new FileReader("file.txt");
            BufferedReader br = new BufferedReader(file);
            System.out.println(br.readLine());
            br.close();
        
        }
    
        public static void main(String[] args) {
            try {
                readFile();  // Handling the checked exception
            } catch (IOException e) {
                System.out.println("Error: " + e.getMessage());
            }
        }
    }

2. What are Unchecked (Runtime) Exceptions?

  • What? Subclasses of RuntimeException
  • When? Compiler doesn’t force you to handle them
  • Checked? Nope — they're unchecked at compile-time
  • Why? These represent programming bugs or logic errors
  • Common Unchecked Exceptions:
    • NullPointerException – Operation on a null variable
    • ArithmeticException – Divide by zero exception
    • ArrayIndexOutOfBoundsException – Bad array index
    • IllegalArgumentException – Invalid method argument
    class UncheckedExample {
        public static void main(String[] args) {
            String text = null;
            
            // ❌ NullPointerException
            System.out.println(text.length());  
        }
    }

How to Choose Between Checked and Unchecked Exceptions?

  • Use Checked – When the caller can and should handle the issue
    • Example: Missing file, invalid input, network timeout
    • Forces the developer to acknowledge the risk
  • Use Unchecked – When the issue is a bug or misuse of API
    • Example: NullPointerException, IndexOutOfBoundsException
    • Caller usually can’t recover

What Are Chained Exceptions? #


  • What? Chained exceptions allow you to link one exception to another
  • Why? To show the original cause of an exception
  • How? Use initCause() method
  • Helps With? Debugging and tracing multi-layered errors
  • Common Use Case:
    • You catch a low-level exception and wrap it inside a higher-level one
    • This way, you don’t lose the original stack trace
  • Example:
    public class ChainedExceptionDemo {
        public static void main(String[] args) {
            try {
                validate(null);
            } catch (Exception e) {
                System.out.println("Exception: " + e);
                System.out.println("Caused by: " 
                                    + e.getCause());
            }
        }
    
        static void validate(String name) throws Exception {
            try {
                if (name == null) {
                    throw new NullPointerException(
                                    "Username is null");
                }
            } catch (NullPointerException e) {
                Exception wrapped 
                            = new Exception(
                                "User validation failed");
                
                // ✅ Linking original cause
                wrapped.initCause(e); 
                
                throw wrapped;
            }
        }
    }

What Are the Best Practices for Handling Exceptions in Java? #


  • 1. Never Hide Exceptions – Always log them, even if you re-throw or recover
    • Why? Silent failures are hard to debug
    • Bad Example:
      try {
          riskyOperation();
      } catch (Exception e) {
          // ❌ Nothing logged, issue disappears
      }
    • Good Example:
      try {
          riskyOperation();
      } catch (Exception e) {
          
          // ✅ Log the exception
          logger.error("Something went wrong", e); 
          
          throw e; // Optional: rethrow if needed
      }
  • 2. User-Friendly Messages – Show clear, non-technical messages to users
    • Why? Users shouldn't see scary stack traces
    • Example (Spring Controller):
      @GetMapping("/item")
      public ResponseEntity<?> getItem() {
          try {
              return ResponseEntity.ok(service.getItem());
          } catch (ItemNotFoundException e) {
              return ResponseEntity
                      .status(404)
                      .body("Item not found." +
                           " Please check the ID.");
          }
      }
  • 3. Provide Debugging Info – Log stack traces and context for developers
    • Why? Helps devs trace root cause
    • Example:
      try {
          processData();
      } catch (Exception e) {
          logger.error(
              "Failed to process data for user: " 
              + userId, e);
      }
  • 4. Global Exception Handling – Use centralized mechanisms (like @ControllerAdvice in Spring)
    • Why? Avoids duplicate try-catch blocks everywhere
    • Example:
      @RestControllerAdvice
      public class GlobalExceptionHandler {
      
          @ExceptionHandler(IllegalArgumentException.class)
          public ResponseEntity<String> handleBadInput(
                                          Exception e) {
              return ResponseEntity
                      .badRequest()
                      .body(
                        "Invalid input. Please try again.");
          }
      }
  • 5. Avoid Misusing Exceptions – Don’t use exceptions for expected logic like looping or validation
    • Why? Bad for performance and readability
    • Bad Example:
      while (true) {
          try {
              return Integer.parseInt(input);
          } catch (NumberFormatException e) {
              // ❌ Using exception to control loop
              input = getNewInput(); 
          }
      }
    • Good Example:
      // ✅ Validate before parsing
      while (!isNumeric(input)) {
          input = getNewInput(); 
      }
      return Integer.parseInt(input);

What Are the Newer Java Features That Help with Exception Handling? #


1. Try-With-Resources (Automatic Resource Management)

  • What? A try block that automatically closes resources
  • Why? No need for manual finally cleanup
  • When? Use with classes that implement AutoCloseable
  • Benefit? Less boilerplate, fewer resource leaks
  • Common Resources:
    • BufferedReader, FileInputStream, Connection, Scanner
    try (BufferedReader br = 
        new BufferedReader(
            new FileReader("file.txt"))) {
        
        System.out.println(br.readLine());
    
    } catch (IOException e) {
    
        e.printStackTrace();
    
    }
    
    //Use with classes that implement AutoCloseable
    public interface AutoCloseable {
        void close() throws Exception;
    }

2. Multi-Catch Block

  • What? Handle multiple exception types in one catch block
  • How? Use | to separate exception classes
  • Why? Avoids repeating the same logic for different exceptions
  • When? When multiple exceptions need the same handling
    try {
        methodThatThrowsException();
    
    // Multi-catch block
    } catch (IOException | SQLException ex) {  
        System.err.println(
            "Exception occurred: " 
            + ex.getMessage());
    }

3. Optional.orElseThrow()

  • What? Throws an exception if the value is absent
  • Why? Avoids null checks and improves readability
  • When? Use when a value is required and absence is exceptional
  • Benefit? Cleaner, expressive, and avoids if (optional.isPresent())
    Optional<String> name = Optional.ofNullable(null);
    String result = name.orElseThrow(
        () -> new IllegalArgumentException(
                            "Value is missing"));