โ˜• Java

Java Try-Catch โ€” Exception Handling, Custom Exceptions & Best Practices

A complete guide to Java exception handling โ€” exception hierarchy, try-catch-finally, multi-catch, try-with-resources, throw vs throws, custom exception design, exception chaining, anti-patterns, and production-ready patterns used in enterprise Java.

๐Ÿ“…

Last Updated

March 2026

โฑ๏ธ

Read Time

26 min

๐ŸŽฏ

Level

Intermediate

๐Ÿท๏ธ

Chapter

23 of 35

What is an Exception in Java?

An exception in Java is an event that disrupts the normal flow of program execution at runtime. It is not a compile-time problem โ€” the code compiles perfectly โ€” but when the program runs, an unexpected condition arises: a file is missing, a network times out, a user passes a null where an object was expected, or the program attempts to divide by zero. Without a mechanism to handle these, the program simply crashes with a stack trace.

Java models exceptions as objects. When an exception occurs, the JVM creates an instance of the appropriate exception class, populates it with a message, the current stack trace, and optionally a cause โ€” and then throws it. The runtime searches the call stack for a matching catch block. If one is found, execution jumps to that handler. If none is found anywhere in the call stack, the default exception handler terminates the thread and prints the stack trace.

Java's exception handling has two core goals: separation of concerns (keep error-handling code away from business logic) and structured recovery (give the program a chance to clean up resources and communicate failure meaningfully, rather than simply crashing). Done well, exception handling makes code significantly more readable, maintainable, and resilient.

Exception Hierarchy โ€” The Throwable Family Tree

Every throwable object in Java descends from java.lang.Throwable. Understanding the hierarchy is essential โ€” it explains why some exceptions must be caught, why others cannot be caught (or shouldn't be), and how a catch(Exception e) block differs from catch(Throwable t).

โ˜• JavaExceptionHierarchy โ€” The Complete Tree
java.lang.Throwable                    โ† Root of everything throwable
โ”œโ”€โ”€ java.lang.Error                    โ† JVM-level problems โ€” do NOT catch
โ”‚   โ”œโ”€โ”€ OutOfMemoryError               โ† JVM ran out of heap memory
โ”‚   โ”œโ”€โ”€ StackOverflowError             โ† Infinite recursion consumed call stack
โ”‚   โ”œโ”€โ”€ VirtualMachineError            โ† Severe JVM internal error
โ”‚   โ”œโ”€โ”€ AssertionError                 โ† assert statement failed
โ”‚   โ””โ”€โ”€ LinkageError                   โ† Class loading / linking problem
โ”‚       โ””โ”€โ”€ NoClassDefFoundError       โ† Class found at compile time but not runtime
โ”‚
โ””โ”€โ”€ java.lang.Exception               โ† Application-level problems
    โ”œโ”€โ”€ java.lang.RuntimeException    โ† UNCHECKED โ€” compiler does not enforce
    โ”‚   โ”œโ”€โ”€ NullPointerException      โ† Dereference of null reference
    โ”‚   โ”œโ”€โ”€ ArrayIndexOutOfBoundsException โ† Array index out of range
    โ”‚   โ”œโ”€โ”€ ClassCastException        โ† Invalid type cast
    โ”‚   โ”œโ”€โ”€ ArithmeticException       โ† e.g., division by zero
    โ”‚   โ”œโ”€โ”€ IllegalArgumentException  โ† Method received illegal argument
    โ”‚   โ”œโ”€โ”€ IllegalStateException     โ† Method called at wrong time
    โ”‚   โ”œโ”€โ”€ NumberFormatException     โ† Invalid string โ†’ number conversion
    โ”‚   โ”œโ”€โ”€ IndexOutOfBoundsException โ† General index out of range
    โ”‚   โ”‚   โ””โ”€โ”€ StringIndexOutOfBoundsException
    โ”‚   โ”œโ”€โ”€ UnsupportedOperationException โ† Operation not supported
    โ”‚   โ”œโ”€โ”€ ConcurrentModificationException โ† Collection modified during iteration
    โ”‚   โ””โ”€โ”€ NoSuchElementException    โ† Iterator has no more elements
    โ”‚
    โ”œโ”€โ”€ IOException                   โ† CHECKED โ€” I/O operation failed
    โ”‚   โ”œโ”€โ”€ FileNotFoundException     โ† File does not exist
    โ”‚   โ””โ”€โ”€ SocketException           โ† Network socket error
    โ”œโ”€โ”€ SQLException                  โ† CHECKED โ€” Database operation failed
    โ”œโ”€โ”€ ParseException                โ† CHECKED โ€” Parsing failed
    โ”œโ”€โ”€ CloneNotSupportedException    โ† CHECKED โ€” clone() not supported
    โ””โ”€โ”€ InterruptedException          โ† CHECKED โ€” Thread was interrupted

Checked vs Unchecked Exceptions โ€” The Compiler's Contract

Java's most debated design decision is the checked exception system. Checked exceptions force the compiler to ensure every caller either handles or explicitly propagates certain exceptions. The philosophy: some failures (missing file, network unavailable, DB connection failed) are foreseeable โ€” the API designer says 'caller: you MUST think about this failure path.' Unchecked exceptions represent programming errors or unrecoverable states that callers typically cannot fix.

AspectChecked ExceptionUnchecked Exception
SuperclassException (but NOT RuntimeException)RuntimeException or Error
Compiler enforcementโœ… Must catch or declare with throwsโŒ Optional โ€” compiler does not check
RepresentsForeseeable, recoverable external conditionsProgramming bugs or unrecoverable JVM states
ExamplesIOException, SQLException, ParseExceptionNullPointerException, IllegalArgumentException
When to use (custom)Caller can and should handle the conditionProgramming contract violated (precondition failed)
Spring / modern JavaMany frameworks wrap in unchecked equivalentsPreferred in modern APIs โ€” less boilerplate
Method signatureMust appear in throws clauseOptionally appears in throws clause (documentation)
Try-catch required?โœ… Yes โ€” compile error if absentโŒ No โ€” but still catchable
Typical recoveryLog + retry / fallback / show user messageFix the bug โ€” not meant to be caught in normal flow
โ˜• JavaCheckedVsUnchecked.java
import java.io.*;
import java.sql.*;

public class CheckedVsUnchecked {

    // โœ… Checked โ€” IOException must be caught or declared
    public String readFileContent(String path) throws IOException {
        // FileNotFoundException is a checked exception (subclass of IOException)
        // If we don't throw IOException, we MUST catch it here โ€” compiler enforces this
        try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
            StringBuilder content = new StringBuilder();
            String line;
            while ((line = reader.readLine()) != null) {
                content.append(line).append("\n");
            }
            return content.toString();
        }
        // Removing 'throws IOException' without a try-catch โ†’ COMPILE ERROR
    }

    // โœ… Unchecked โ€” IllegalArgumentException doesn't require declaration
    public double divide(double numerator, double denominator) {
        if (denominator == 0) {
            // No 'throws ArithmeticException' needed in signature
            throw new IllegalArgumentException(
                "Denominator cannot be zero โ€” received: " + denominator);
        }
        return numerator / denominator;
    }

    // โœ… Both in the same method
    public void saveUserToDatabase(User user) throws SQLException {
        // Precondition check โ€” unchecked (programming contract)
        if (user == null) {
            throw new IllegalArgumentException("User cannot be null");
        }
        if (user.getName() == null || user.getName().isBlank()) {
            throw new IllegalArgumentException("User name cannot be blank");
        }
        // Database failure โ€” checked (external, recoverable)
        try (Connection conn = getConnection()) {
            PreparedStatement ps = conn.prepareStatement(
                "INSERT INTO users (name, email) VALUES (?, ?)");
            ps.setString(1, user.getName());
            ps.setString(2, user.getEmail());
            ps.executeUpdate();
        }
        // SQLException propagated to caller who can decide: retry, log, alert
    }

    private Connection getConnection() throws SQLException {
        return DriverManager.getConnection("jdbc:...", "user", "pass");
    }
}

try-catch Block โ€” Basic Syntax & Execution Flow

The try-catch block is the foundation of Java exception handling. Code that might throw an exception goes inside the try block. If an exception is thrown, the JVM immediately stops executing the try block at the point of the exception (remaining statements in the try block are skipped) and searches for a matching catch block. If a match is found, that handler executes and then program execution continues normally after the entire try-catch construct.

โ˜• JavaTryCatchBasics.java
public class TryCatchBasics {
    public static void main(String[] args) {

        // โœ… Basic try-catch
        try {
            int result = 10 / 0;            // Throws ArithmeticException
            System.out.println(result);     // โ† NEVER reached
        } catch (ArithmeticException e) {
            System.out.println("Cannot divide by zero: " + e.getMessage());
        }
        System.out.println("Program continues..."); // โ† Always reached

        // โœ… The exception object carries useful info
        try {
            String text = null;
            int length = text.length(); // Throws NullPointerException
        } catch (NullPointerException e) {
            System.out.println("Exception type:    " + e.getClass().getName());
            System.out.println("Message:           " + e.getMessage());
            System.out.println("Stack trace:");
            e.printStackTrace(); // Prints to System.err
        }

        // โœ… Catching a superclass catches all subclasses
        try {
            int[] arr = new int[3];
            arr[10] = 99; // Throws ArrayIndexOutOfBoundsException
        } catch (RuntimeException e) {
            // Catches ArrayIndexOutOfBoundsException and ALL RuntimeExceptions
            System.out.println("Caught: " + e.getClass().getSimpleName());
        }

        // โœ… When no exception is thrown โ€” catch block is SKIPPED
        try {
            int result = 10 / 2;             // No exception
            System.out.println("Result: " + result); // 5 โ€” this prints
        } catch (ArithmeticException e) {
            System.out.println("This never prints"); // Skipped
        }
        // Output: Result: 5

        // โœ… Exception object methods
        try {
            Integer.parseInt("not-a-number");
        } catch (NumberFormatException e) {
            System.out.println(e.getMessage());      // For input string: "not-a-number"
            System.out.println(e.getClass().getName()); // java.lang.NumberFormatException
            // e.getCause() โ€” the causing exception (null if none)
            // e.getStackTrace() โ€” StackTraceElement[]
            // e.toString() โ€” class name + message
        }
    }
}

Multiple catch Blocks โ€” The Specificity Ladder

A single try block can have multiple catch blocks, each handling a different exception type. Java evaluates them top-to-bottom and executes the first matching catch block โ€” all subsequent catch blocks are skipped. This creates a critical ordering rule: more specific exceptions must come before less specific ones. Placing a superclass catch block before a subclass causes a compile error.

โ˜• JavaMultipleCatchBlocks.java
import java.io.*;

public class MultipleCatchBlocks {

    public void parseAndProcessFile(String filePath, String numberStr) {

        try {
            // Multiple potential failure points
            BufferedReader reader = new BufferedReader(new FileReader(filePath));
            String content = reader.readLine();
            int number = Integer.parseInt(numberStr);
            int result = 100 / number;
            System.out.println("Result: " + result);

        } catch (FileNotFoundException e) {
            // Most specific IOException subclass โ€” must come BEFORE IOException
            System.err.println("File not found: " + filePath);
            System.err.println("Please check the file path and try again.");

        } catch (IOException e) {
            // General I/O failure โ€” comes AFTER FileNotFoundException
            System.err.println("Failed to read file: " + e.getMessage());

        } catch (NumberFormatException e) {
            // Parsing failure
            System.err.println("'" + numberStr + "' is not a valid integer.");

        } catch (ArithmeticException e) {
            // Division by zero
            System.err.println("Cannot divide by zero. Provide a non-zero number.");

        } catch (Exception e) {
            // Catch-all โ€” most general, MUST come last
            System.err.println("Unexpected error: " + e.getMessage());
            e.printStackTrace();
        }
    }

    // โŒ COMPILE ERROR โ€” unreachable catch block
    public void wrongOrder() {
        try {
            new FileReader("file.txt");
        } catch (IOException e) {          // IOException is superclass of
            System.out.println("IO");
        } catch (FileNotFoundException e) { // โŒ COMPILE ERROR โ€” already caught above!
            System.out.println("File");   // FileNotFoundException extends IOException
        }
    }
}

Multi-catch (Java 7+) โ€” One Block, Many Exception Types

Java 7 introduced multi-catch โ€” a single catch block that handles multiple unrelated exception types using the pipe (|) operator. This is ideal when different exception types require the same handling logic โ€” eliminating code duplication without compromising specificity. Multi-catch is a pure syntax improvement: at bytecode level it generates the same structure as separate catch blocks.

โ˜• JavaMultiCatch.java
import java.io.*;
import java.sql.*;
import java.lang.reflect.*;

public class MultiCatch {

    public void processData(String input) {

        // โŒ BEFORE Java 7 โ€” repetitive catch blocks with identical handling
        try {
            // ... code ...
        } catch (IOException e) {
            logError(e);
            notifyAdmin(e);
        } catch (SQLException e) {
            logError(e);   // Exact same code โ€” copy-paste
            notifyAdmin(e);
        } catch (ParseException e) {
            logError(e);   // Exact same code โ€” copy-paste
            notifyAdmin(e);
        }

        // โœ… Java 7+ โ€” multi-catch with | operator
        try {
            performOperation(input);
        } catch (IOException | SQLException | ParseException e) {
            // 'e' is effectively final here โ€” cannot be reassigned
            logError(e);
            notifyAdmin(e);
        }

        // โœ… Multi-catch with different handling for different groups
        try {
            riskyOperation(input);
        } catch (NullPointerException | IllegalArgumentException e) {
            // Input validation failures โ€” client error
            System.err.println("Invalid input: " + e.getMessage());
            throw new BadRequestException("Invalid request data", e);
        } catch (IOException | SQLException e) {
            // Infrastructure failures โ€” server error
            System.err.println("Infrastructure failure: " + e.getMessage());
            throw new ServiceUnavailableException("Service temporarily unavailable", e);
        }
    }

    // โŒ Multi-catch does NOT allow superclass and subclass together
    public void invalidMultiCatch() {
        try {
            new FileReader("x");
        } catch (IOException | FileNotFoundException e) { // โŒ COMPILE ERROR
            // FileNotFoundException is a subclass of IOException โ€”
            // they cannot appear together in multi-catch (redundant)
        }
    }

    private void performOperation(String s) throws IOException, SQLException, ParseException {}
    private void riskyOperation(String s) throws IOException, SQLException {}
    private void logError(Throwable t) {}
    private void notifyAdmin(Throwable t) {}
}

finally Block โ€” The Guaranteed Cleanup

The finally block is a block of code that always executes after the try block completes โ€” whether it completed normally, threw an exception that was caught, threw an exception that was not caught, or exited via return, break, or continue. It is Java's mechanism for guaranteed cleanup: releasing resources, closing connections, releasing locks, or resetting state โ€” regardless of what happened in the try block.

โ˜• JavaFinallyBlock.java
import java.io.*;

public class FinallyBlock {

    // โœ… Classic use case: resource cleanup before try-with-resources
    public String readFileLegacy(String path) throws IOException {
        BufferedReader reader = null;
        try {
            reader = new BufferedReader(new FileReader(path));
            return reader.readLine();
        } catch (FileNotFoundException e) {
            System.err.println("File not found: " + path);
            return null;
        } finally {
            // โœ… Always runs โ€” whether exception was thrown or not
            if (reader != null) {
                try {
                    reader.close();
                } catch (IOException e) {
                    System.err.println("Failed to close reader: " + e.getMessage());
                }
            }
            System.out.println("Cleanup complete."); // Always prints
        }
    }

    // โœ… finally with return โ€” TRICKY behaviour
    public int trickyReturn() {
        try {
            System.out.println("Try block");
            return 1;             // โ† Starts to return 1...
        } finally {
            System.out.println("Finally block"); // โ† ...but this runs first
            return 2;             // โ† Return in finally OVERRIDES return in try!
        }
        // Output: Try block โ†’ Finally block
        // Actual return value: 2  โ† finally wins
    }

    // โœ… finally with exception โ€” also tricky
    public void trickyException() throws Exception {
        try {
            throw new IOException("From try");
        } finally {
            throw new RuntimeException("From finally"); // โ† Suppresses IOException!
        }
        // Only RuntimeException propagates โ€” IOException is SILENTLY LOST
        // This is why throwing from finally is an anti-pattern
    }

    // โœ… finally without catch โ€” try-finally (no catch needed)
    public void updateWithLock(ReentrantLock lock) {
        lock.lock();
        try {
            performUpdate(); // If this throws, lock still gets released
        } finally {
            lock.unlock(); // ALWAYS releases the lock
        }
    }

    // โŒ When does finally NOT run? Only in these rare cases:
    public void whenFinallySkips() {
        try {
            System.exit(0); // โ† JVM terminates โ€” finally DOES NOT run
        } finally {
            System.out.println("This never prints when System.exit() is called");
        }
    }

    private void performUpdate() {}
    static class ReentrantLock {
        void lock() {}
        void unlock() {}
    }
}

try-with-resources (Java 7+) โ€” AutoCloseable Done Right

Introduced in Java 7, try-with-resources is the modern, correct way to manage resources that must be closed after use. Any object implementing java.lang.AutoCloseable (or its subinterface java.io.Closeable) can be declared in the try header โ€” Java guarantees that close() is called on each resource when the block exits, in reverse order of declaration, whether the block completes normally or via exception. This is strictly superior to manually-coded try-finally for resource management.

โ˜• JavaTryWithResources.java
import java.io.*;
import java.sql.*;

public class TryWithResources {

    // โœ… Single resource โ€” simplest form
    public String readFirstLine(String path) throws IOException {
        try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
            return reader.readLine();
            // reader.close() is called automatically here โ€” always
        }
        // Compare: no finally block needed at all
    }

    // โœ… Multiple resources โ€” closed in REVERSE order of declaration
    public void copyFile(String sourcePath, String destPath) throws IOException {
        try (FileInputStream  fis  = new FileInputStream(sourcePath);   // opened 1st
             FileOutputStream fos  = new FileOutputStream(destPath);    // opened 2nd
             BufferedInputStream  in  = new BufferedInputStream(fis);   // opened 3rd
             BufferedOutputStream out = new BufferedOutputStream(fos)) {// opened 4th

            byte[] buffer = new byte[8192];
            int bytesRead;
            while ((bytesRead = in.read(buffer)) != -1) {
                out.write(buffer, 0, bytesRead);
            }
            // Close order: out โ†’ in โ†’ fos โ†’ fis  (reverse of opening)
        }
    }

    // โœ… JDBC โ€” try-with-resources for connections, statements, result sets
    public User findUserById(Connection conn, long userId) throws SQLException {
        String sql = "SELECT id, name, email FROM users WHERE id = ?";

        try (PreparedStatement ps   = conn.prepareStatement(sql);
             // Note: Connection is NOT declared here โ€” it's passed in and
             // managed by the connection pool, not closed here
             ) {

            ps.setLong(1, userId);

            try (ResultSet rs = ps.executeQuery()) {
                if (rs.next()) {
                    return new User(rs.getLong("id"), rs.getString("name"),
                                   rs.getString("email"));
                }
                return null;
            }
            // rs.close() called automatically
        }
        // ps.close() called automatically
    }

    // โœ… Java 9+: Effectively-final variables in try header (no redeclaration)
    public void java9Style() throws IOException {
        BufferedReader reader = new BufferedReader(new FileReader("data.txt"));
        // In Java 9+, effectively-final variables can be used directly:
        try (reader) {            // No need to redeclare โ€” Java 9+
            System.out.println(reader.readLine());
        }
    }

    // โœ… Custom AutoCloseable โ€” any class can participate
    static class DatabaseTransaction implements AutoCloseable {
        private final Connection connection;
        private boolean committed = false;

        DatabaseTransaction(Connection connection) throws SQLException {
            this.connection = connection;
            this.connection.setAutoCommit(false);
            System.out.println("Transaction started");
        }

        public void commit() throws SQLException {
            connection.commit();
            committed = true;
        }

        @Override
        public void close() throws SQLException {
            if (!committed) {
                System.out.println("Auto-rolling back transaction");
                connection.rollback(); // Rollback if not committed
            }
            connection.setAutoCommit(true);
        }
    }

    public void transferFunds(Connection conn, long fromId,
                              long toId, double amount) throws SQLException {
        try (DatabaseTransaction tx = new DatabaseTransaction(conn)) {
            deductBalance(conn, fromId, amount);
            addBalance(conn, toId, amount);
            tx.commit(); // Only commits if both succeed
            // If exception occurs before commit โ†’ auto-rollback via close()
        }
    }

    private void deductBalance(Connection c, long id, double amt) throws SQLException {}
    private void addBalance(Connection c, long id, double amt) throws SQLException {}
    private static class User {
        User(long id, String name, String email) {}
    }
}

throw โ€” Firing an Exception Manually

The throw keyword explicitly throws an exception from your code โ€” interrupting the current execution flow at that point. It is used to signal that a method's precondition has been violated, a business rule has been broken, or an operation cannot proceed due to an invalid state. You can throw any instance of Throwable, but in practice you always throw Exception subclasses โ€” never Error subclasses.

โ˜• JavaThrowKeyword.java
public class ThrowKeyword {

    // โœ… Throwing unchecked โ€” precondition validation (guard clause pattern)
    public void setAge(int age) {
        if (age < 0 || age > 150) {
            throw new IllegalArgumentException(
                "Age must be between 0 and 150. Received: " + age);
        }
        this.age = age;
    }

    // โœ… Throwing unchecked โ€” null check (prefer Objects.requireNonNull)
    public void processOrder(Order order) {
        if (order == null) {
            throw new NullPointerException("order must not be null");
        }
        // โœ… Better idiom (Java 7+):
        java.util.Objects.requireNonNull(order, "order must not be null");
        java.util.Objects.requireNonNull(order.getId(), "order ID must not be null");
    }

    // โœ… Throwing checked โ€” callers must handle or declare
    public byte[] readBytes(String path) throws IOException {
        java.io.File file = new java.io.File(path);
        if (!file.exists()) {
            throw new java.io.FileNotFoundException(
                "File not found at path: " + path);
        }
        if (!file.canRead()) {
            throw new java.io.IOException(
                "No read permission for file: " + path);
        }
        return java.nio.file.Files.readAllBytes(file.toPath());
    }

    // โœ… Re-throwing โ€” catch, do something, rethrow same exception
    public void processWithLogging(String data) throws IOException {
        try {
            riskyOperation(data);
        } catch (IOException e) {
            System.err.println("Failed to process: " + data); // Log
            throw e; // Rethrow โ€” caller still handles it
        }
    }

    // โœ… Wrapping โ€” catch one type, throw another with more context
    public User loadUser(String userId) {
        try {
            return userRepository.findById(userId);
        } catch (SQLException e) {
            // Translate infrastructure exception to domain exception
            throw new UserServiceException(
                "Failed to load user with ID: " + userId, e); // 'e' becomes the cause
        }
    }

    // โœ… throw in a conditional chain โ€” useful for state machines
    public void cancelOrder(Order order) {
        if (order.getStatus() == OrderStatus.DELIVERED) {
            throw new IllegalStateException(
                "Cannot cancel order " + order.getId() + " โ€” already delivered.");
        }
        if (order.getStatus() == OrderStatus.CANCELLED) {
            throw new IllegalStateException(
                "Order " + order.getId() + " is already cancelled.");
        }
        order.setStatus(OrderStatus.CANCELLED);
    }

    private int age;
    private Object userRepository;
    private void riskyOperation(String s) throws IOException {}
    private static class Order {
        Object getId() { return null; }
        OrderStatus getStatus() { return null; }
        void setStatus(OrderStatus s) {}
    }
    private enum OrderStatus { DELIVERED, CANCELLED }
}

throws โ€” Declaring Propagated Exceptions

The throws keyword in a method signature declares which checked exceptions the method might propagate to its caller. It is a contract: 'I, this method, might produce these exception types โ€” caller, you must deal with them.' It does NOT handle the exception โ€” it propagates it up the call stack. The compiler requires this declaration for checked exceptions; it is optional but recommended for unchecked exceptions when they are part of the documented API contract.

โ˜• JavaThrowsKeyword.java
import java.io.*;
import java.sql.*;

public class ThrowsKeyword {

    // โœ… Declaring a single checked exception
    public String readFile(String path) throws IOException {
        return new String(java.nio.file.Files.readAllBytes(
            java.nio.file.Paths.get(path)));
    }

    // โœ… Declaring multiple checked exceptions
    public User authenticateAndLoad(String token) throws IOException, SQLException {
        String userId = validateToken(token);    // throws IOException
        return loadFromDatabase(userId);         // throws SQLException
    }

    // โœ… Propagating up a chain of methods
    // Level 3 โ€” throws declared
    private String validateToken(String token) throws IOException {
        // Calls a method that throws IOException
        return readFile("/tokens/" + token);
    }

    // Level 2 โ€” propagates declared exception
    private User loadFromDatabase(String userId) throws SQLException {
        Connection conn = DriverManager.getConnection("jdbc:...");
        // ...query...
        return null;
    }

    // โœ… throws in interface / abstract class โ€” callers of ALL implementations must handle
    interface DataRepository {
        User findById(long id) throws SQLException;
        void save(User user) throws SQLException;
    }

    // โœ… Overriding: subclass can declare FEWER exceptions, not more
    static class BaseService {
        public void execute() throws IOException, SQLException { }
    }

    static class ConcreteService extends BaseService {
        @Override
        public void execute() throws IOException { } // โœ… Removed SQLException โ€” allowed
        // throws IOException, SQLException, RuntimeException โ€” โœ… can add unchecked
        // throws IOException, SQLException, Exception โ€” โŒ COMPILE ERROR: broader checked
    }

    // โœ… Documenting unchecked exceptions in throws (optional but professional)
    /**
     * @throws IllegalArgumentException if amount is negative or zero
     * @throws InsufficientFundsException if account balance is insufficient
     */
    public void withdraw(double amount) throws InsufficientFundsException {
        if (amount <= 0) {
            throw new IllegalArgumentException("Withdrawal amount must be positive");
        }
        if (this.balance < amount) {
            throw new InsufficientFundsException(this.balance, amount);
        }
        this.balance -= amount;
    }

    private double balance = 5000;
    private static class User {}
    private static class InsufficientFundsException extends RuntimeException {
        InsufficientFundsException(double balance, double amount) {
            super(String.format("Insufficient funds. Balance: %.2f, Requested: %.2f",
                balance, amount));
        }
    }
}
Aspectthrowthrows
What it doesActually throws an exception instance at runtimeDeclares which checked exceptions a method may propagate
LocationInside method body โ€” executable statementIn method signature โ€” after parameter list
Syntaxthrow new SomeException("message");public void method() throws IOException, SQLException
ArgumentSingle exception object (instance)Comma-separated list of exception CLASS names
Required forManually signalling failure in your codeChecked exceptions that escape the method
Unchecked required?No declaration needed โ€” just throwOptional (for documentation)
Checked required?throw the instance (required to signal the failure)throws the class (required in signature if not caught)
Causes executionโœ… Yes โ€” immediately interrupts normal flowโŒ No โ€” only informs the compiler and callers

Custom Exceptions โ€” Domain-Specific Error Vocabulary

Standard Java exceptions are general-purpose โ€” IOException tells you something went wrong with I/O but not what or why in your domain. Custom exceptions give your application its own error vocabulary: InsufficientFundsException, UserNotFoundException, PaymentGatewayException. They carry domain-specific data, produce meaningful log messages, and allow callers to handle specific failure types precisely.

๐Ÿ—๏ธ
Rule 1 โ€” Extend the Right Base

For checked exceptions (callers must handle): extend Exception. For unchecked (programming errors / domain invariants): extend RuntimeException. For errors the JVM should never recover from: extend Error (almost never done in application code). Modern preference: lean towards RuntimeException subclasses โ€” most frameworks (Spring, Hibernate) use unchecked exceptions to avoid cluttering service interfaces with checked exception declarations.

๐Ÿ“‹
Rule 2 โ€” Provide All Four Constructors

Always provide: (1) Exception(String message), (2) Exception(String message, Throwable cause), (3) Exception(Throwable cause), (4) Exception() โ€” the no-arg. These mirror the standard Java exception pattern. The cause constructors are critical for exception chaining โ€” wrapping a lower-level exception in a domain exception without losing the original stack trace.

๐Ÿ’Ž
Rule 3 โ€” Carry Domain-Specific Context

The best custom exceptions carry more than just a string message โ€” they carry the DOMAIN DATA that caused the failure. InsufficientFundsException should carry the available balance and requested amount. UserNotFoundException should carry the userId that wasn't found. This data enables logging systems, monitoring tools, and error handlers to produce actionable alerts without parsing string messages.

๐Ÿ”’
Rule 4 โ€” Make Fields Final and Immutable

Exception fields should be final โ€” exceptions are value objects representing a single failure event. Making fields mutable allows them to be modified after creation, which destroys the immutability invariant and creates debugging nightmares. Always set all custom fields in the constructor and provide only getters.

โ˜• JavaCustomExceptions.java โ€” Production-Grade Design
// โœ… Base domain exception โ€” all app exceptions extend this
public class AppException extends RuntimeException {
    private final String errorCode;

    public AppException(String errorCode, String message) {
        super(message);
        this.errorCode = errorCode;
    }
    public AppException(String errorCode, String message, Throwable cause) {
        super(message, cause);
        this.errorCode = errorCode;
    }
    public String getErrorCode() { return errorCode; }
}

// โœ… Domain-specific: carries failure context as typed fields
public class InsufficientFundsException extends AppException {

    private final double availableBalance;
    private final double requestedAmount;
    private final String accountId;

    public InsufficientFundsException(String accountId,
                                       double availableBalance,
                                       double requestedAmount) {
        super("INSUFFICIENT_FUNDS",
            String.format("Account %s has insufficient funds. " +
                "Available: %.2f, Requested: %.2f",
                accountId, availableBalance, requestedAmount));
        this.accountId        = accountId;
        this.availableBalance = availableBalance;
        this.requestedAmount  = requestedAmount;
    }

    public double getAvailableBalance() { return availableBalance; }
    public double getRequestedAmount()  { return requestedAmount; }
    public String getAccountId()         { return accountId; }
    public double getDeficit()           { return requestedAmount - availableBalance; }
}

// โœ… Resource-not-found exception โ€” generic, parameterised
public class ResourceNotFoundException extends AppException {

    private final String resourceType;
    private final Object resourceId;

    public ResourceNotFoundException(String resourceType, Object resourceId) {
        super("RESOURCE_NOT_FOUND",
            resourceType + " not found with ID: " + resourceId);
        this.resourceType = resourceType;
        this.resourceId   = resourceId;
    }

    // Convenient static factories
    public static ResourceNotFoundException forUser(long userId) {
        return new ResourceNotFoundException("User", userId);
    }
    public static ResourceNotFoundException forOrder(String orderId) {
        return new ResourceNotFoundException("Order", orderId);
    }

    public String getResourceType() { return resourceType; }
    public Object getResourceId()   { return resourceId; }
}

// โœ… Checked exception โ€” external API call failed
public class PaymentGatewayException extends Exception {

    private final int    gatewayErrorCode;
    private final String transactionId;
    private final boolean retryable;

    public PaymentGatewayException(int gatewayErrorCode, String transactionId,
                                   boolean retryable, String message) {
        super(message);
        this.gatewayErrorCode = gatewayErrorCode;
        this.transactionId    = transactionId;
        this.retryable        = retryable;
    }
    public PaymentGatewayException(int code, String txId,
                                   boolean retryable, String msg, Throwable cause) {
        super(msg, cause);
        this.gatewayErrorCode = code;
        this.transactionId    = txId;
        this.retryable        = retryable;
    }

    public int    getGatewayErrorCode() { return gatewayErrorCode; }
    public String getTransactionId()    { return transactionId; }
    public boolean isRetryable()        { return retryable; }
}

Exception Chaining โ€” Preserving the Root Cause

Exception chaining (also called exception wrapping) is the practice of catching a lower-level exception and rethrowing a higher-level exception that wraps the original as its cause. This is critical for production debugging: the high-level exception tells you what failed at the domain level; the chained cause tells you why at the infrastructure level โ€” without exposing implementation details to the caller.

โ˜• JavaExceptionChaining.java
public class ExceptionChaining {

    // โŒ BAD: Losing the original cause โ€” debugging becomes impossible
    public User findUserBad(long id) {
        try {
            return userDao.findById(id);
        } catch (SQLException e) {
            // e is swallowed โ€” DBA can never find out what SQL failed
            throw new RuntimeException("Failed to find user");
        }
    }

    // โŒ ALSO BAD: Logging AND rethrowing โ€” the exception gets logged twice
    public User findUserAlsoBad(long id) {
        try {
            return userDao.findById(id);
        } catch (SQLException e) {
            log.error("DB error", e);      // Logged here
            throw new RuntimeException(e);  // AND rethrown โ€” logged again by caller
        }
    }

    // โœ… GOOD: Wrap with cause โ€” original stack trace fully preserved
    public User findUser(long userId) {
        try {
            return userDao.findById(userId);
        } catch (SQLException e) {
            // 'e' becomes the cause โ€” full chain preserved in stack trace
            throw new UserServiceException(
                "Failed to retrieve user with ID: " + userId, e);
        }
    }

    // โœ… Reading the chain โ€” traversing causes programmatically
    public static void printExceptionChain(Throwable t) {
        System.out.println("Exception chain:");
        int level = 0;
        Throwable current = t;
        while (current != null) {
            System.out.println("  ".repeat(level) +
                "[" + level + "] " + current.getClass().getSimpleName() +
                ": " + current.getMessage());
            current = current.getCause();
            level++;
        }
    }

    // โœ… Full chain visible in stack trace output:
    // com.app.UserServiceException: Failed to retrieve user with ID: 42
    //     at com.app.UserService.findUser(UserService.java:35)
    //     ...
    // Caused by: java.sql.SQLException: Connection refused to host: db.example.com
    //     at com.mysql.jdbc.Driver.connect(Driver.java:...)
    //     ...
    // Caused by: java.net.ConnectException: Connection refused
    //     at java.net.Socket.connect(Socket.java:...)

    // โœ… Useful cause-inspection utility
    public static boolean hasCause(Throwable t, Class<? extends Throwable> type) {
        Throwable current = t;
        while (current != null) {
            if (type.isInstance(current)) return true;
            current = current.getCause();
        }
        return false;
    }

    private Object userDao;
    private Object log;
    private static class UserServiceException extends RuntimeException {
        UserServiceException(String msg, Throwable cause) { super(msg, cause); }
    }
}

Exception vs Error โ€” Know the Boundary

Both Exception and Error extend Throwable, but they represent fundamentally different situations with different handling strategies. Understanding this boundary prevents a class of serious bugs: catching Error (or Throwable) and attempting to continue when the JVM is in an inconsistent state.

AspectExceptionError
RepresentsApplication-level failures โ€” recoverable or reportableJVM-level failures โ€” typically unrecoverable
ExamplesIOException, SQLException, NullPointerExceptionOutOfMemoryError, StackOverflowError, AssertionError
Should you catch?โœ… Yes โ€” that's its purposeโŒ Almost never โ€” JVM may be in undefined state
Can you recover?Usually yes โ€” retry, fallback, report to userRarely โ€” JVM is often broken beyond recovery
HierarchyThrowable โ†’ Exception โ†’ (Checked | RuntimeException)Throwable โ†’ Error
Caused byYour code, external systems, bad input, race conditionsJVM internals, memory exhaustion, stack overflow
Common mistakeCatching Exception hides programming bugsCatching Error gives false sense of recovery
When to catch ErrorN/A โ€” don't catchOnly: graceful shutdown handlers, JVM agent code, test frameworks
โ˜• JavaExceptionVsError.java
public class ExceptionVsError {

    // โŒ DANGEROUS: Catching Error โ€” looks safe, is a lie
    public void dangerousCatch() {
        try {
            processHugeDataSet();
        } catch (OutOfMemoryError e) {
            System.out.println("Caught OOM โ€” continuing..."); // FALSE SAFETY
            // Reality: heap is still full, next allocation will fail again
            // JVM may be in corrupt state โ€” undefined behaviour ahead
        }
    }

    // โŒ ALSO DANGEROUS: Catching Throwable in business code
    public void catchThrowable() {
        try {
            businessLogic();
        } catch (Throwable t) { // Catches Error too โ€” almost always wrong
            log(t);
        }
    }

    // โœ… The ONE legitimate use: graceful shutdown handler at app boundary
    public static void main(String[] args) {
        try {
            Application.start();
        } catch (Throwable t) {
            // Top-level catch at JVM entry point โ€” only acceptable location
            System.err.println("FATAL: Application crashed: " + t.getMessage());
            t.printStackTrace();
            System.exit(1); // Explicit exit with error code
        }
    }

    // โœ… StackOverflowError โ€” caused by unguarded recursion
    public int factorial(int n) {
        // โŒ No base case โ€” will throw StackOverflowError
        return n * factorial(n - 1);
    }

    public int factorialSafe(int n) {
        if (n < 0)  throw new IllegalArgumentException("n must be non-negative");
        if (n == 0) return 1; // โœ… Base case prevents StackOverflowError
        return n * factorialSafe(n - 1);
    }

    private void processHugeDataSet() {}
    private void businessLogic() {}
    private void log(Throwable t) {}
    private static class Application { static void start() {} }
}

Bad Practices & Anti-Patterns โ€” What Causes Silent Failures

Exception-handling anti-patterns are uniquely dangerous because they convert loud, visible failures into silent, invisible ones. A program that crashes with a stack trace is bad. A program that swallows exceptions and silently continues with incorrect data is far worse โ€” it corrupts business-critical information while reporting success. These are the most common exception handling failures in production Java code.

๐Ÿšซ
Swallowing Exceptions (Empty catch Block)

catch (Exception e) { } is the single most dangerous pattern in Java. The exception is caught, completely ignored, and execution continues as if nothing happened. The program may produce incorrect results for hours or days before anyone notices. Minimum: always log. Better: log + rethrow, or log + return a safe default that signals failure to the caller.

๐Ÿšซ
Catching Exception or Throwable Too Broadly

catch (Exception e) in a business method catches NullPointerException, ClassCastException, and IndexOutOfBoundsException โ€” programming bugs that should be fixed, not silently handled. Broad catches hide real bugs. Rule: catch only the specific exception types you know how to handle. If you need a safety net, catch Exception at the outermost boundary only โ€” never deep inside business logic.

๐Ÿšซ
Using Exceptions for Flow Control

try { return map.get(key); } catch (NullPointerException e) { return default; } is an abuse of exceptions. Exceptions are for EXCEPTIONAL conditions โ€” rare, unexpected failures. Checking whether a key exists (map.containsKey(key)) is normal flow, not exceptional. Using exceptions for expected conditions is slow (exception creation builds a stack trace โ€” expensive), unreadable, and misleading.

๐Ÿšซ
Logging AND Rethrowing (Double Logging)

catch (Exception e) { log.error('Failed', e); throw e; } causes the same exception to be logged 2-3 times in a layered application. Log files become unreadable โ€” the same error appears in service layer, repository layer, and controller layer logs. Rule: log ONCE at the boundary where you actually HANDLE the exception (not just rethrow). Every other layer just wraps and rethrows.

๐Ÿšซ
Returning null from a catch Block

catch (Exception e) { return null; } shifts the error handling problem to the caller โ€” but without the exception context. The caller gets null back with no indication of why, leading to a NullPointerException far from the original failure. If you must return a value from a catch block, use Optional.empty() or a Result/Either type to communicate failure explicitly.

๐Ÿšซ
Using Exception Message Parsing for Logic

catch (Exception e) { if (e.getMessage().contains('duplicate key')) { ... } } is fragile and wrong. Exception messages are human-readable strings โ€” they change between DB drivers, JVM versions, and locale settings. Instead: catch the specific exception type (SQLIntegrityConstraintViolationException), check the SQL error code (e.getErrorCode()), or better yet, check the constraint BEFORE attempting the operation.

โ˜• JavaExceptionAntiPatterns.java
// โŒ ANTI-PATTERN 1: Swallowing โ€” the silent killer
try {
    saveOrderToDatabase(order);
} catch (Exception e) {
    // NOTHING HERE โ€” order silently not saved, user thinks it was
}

// โœ… BETTER: At minimum, log. Ideally rethrow or return failure signal.
try {
    saveOrderToDatabase(order);
} catch (SQLException e) {
    log.error("Failed to save order {}: {}", order.getId(), e.getMessage(), e);
    throw new OrderPersistenceException("Could not save order " + order.getId(), e);
}

// โŒ ANTI-PATTERN 2: Exception as flow control โ€” slow and misleading
public User findUser(String id) {
    try {
        return userCache.get(id); // Throws if not found
    } catch (CacheException e) {
        return null; // Using exception to signal 'not found' โ€” wrong
    }
}

// โœ… BETTER: Check, don't catch
public Optional<User> findUser(String id) {
    if (userCache.contains(id)) {
        return Optional.of(userCache.get(id));
    }
    return Optional.empty(); // Explicit 'not found' without exception
}

// โŒ ANTI-PATTERN 3: Returning null from catch
public Config loadConfig(String path) {
    try {
        return parseConfigFile(path);
    } catch (IOException e) {
        return null; // Caller has no idea WHY it's null
    }
}

// โœ… BETTER: Throw with context or return Optional
public Config loadConfig(String path) {
    try {
        return parseConfigFile(path);
    } catch (IOException e) {
        throw new ConfigurationException(
            "Cannot load config from: " + path, e);
    }
}

Real-World Production Code Examples โ€” Exception Handling in Enterprise Java

The following examples demonstrate production-grade exception handling in a Spring Boot microservice โ€” covering REST API global error handling, service layer patterns, and repository layer exception translation.

โ˜• JavaGlobalExceptionHandler.java โ€” Spring Boot REST Error Handling
package com.techsustainify.api.exception;

import org.springframework.http.*;
import org.springframework.web.bind.annotation.*;
import java.time.Instant;
import java.util.*;

/**
 * Centralised exception handler for all REST controllers.
 * Translates domain exceptions into HTTP responses.
 * Logs once โ€” no other layer logs these exceptions.
 */
@RestControllerAdvice
public class GlobalExceptionHandler {

    private static final org.slf4j.Logger log =
        org.slf4j.LoggerFactory.getLogger(GlobalExceptionHandler.class);

    // โœ… Domain: Resource not found โ†’ 404
    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleNotFound(ResourceNotFoundException ex) {
        log.warn("Resource not found: {} with ID {}",
            ex.getResourceType(), ex.getResourceId());
        return ResponseEntity
            .status(HttpStatus.NOT_FOUND)
            .body(ErrorResponse.of("RESOURCE_NOT_FOUND", ex.getMessage()));
    }

    // โœ… Domain: Insufficient funds โ†’ 422 Unprocessable Entity
    @ExceptionHandler(InsufficientFundsException.class)
    public ResponseEntity<ErrorResponse> handleInsufficientFunds(
            InsufficientFundsException ex) {
        log.info("Insufficient funds for account {}: balance={}, requested={}",
            ex.getAccountId(), ex.getAvailableBalance(), ex.getRequestedAmount());
        Map<String, Object> details = new LinkedHashMap<>();
        details.put("availableBalance", ex.getAvailableBalance());
        details.put("requestedAmount",  ex.getRequestedAmount());
        details.put("deficit",          ex.getDeficit());
        return ResponseEntity
            .status(HttpStatus.UNPROCESSABLE_ENTITY)
            .body(ErrorResponse.of("INSUFFICIENT_FUNDS", ex.getMessage(), details));
    }

    // โœ… Validation: Bad request input โ†’ 400
    @ExceptionHandler(IllegalArgumentException.class)
    public ResponseEntity<ErrorResponse> handleBadRequest(IllegalArgumentException ex) {
        log.debug("Bad request: {}", ex.getMessage());
        return ResponseEntity
            .status(HttpStatus.BAD_REQUEST)
            .body(ErrorResponse.of("INVALID_REQUEST", ex.getMessage()));
    }

    // โœ… External: Payment gateway failure
    @ExceptionHandler(PaymentGatewayException.class)
    public ResponseEntity<ErrorResponse> handlePaymentGateway(
            PaymentGatewayException ex) {
        log.error("Payment gateway error: code={}, txId={}, retryable={}",
            ex.getGatewayErrorCode(), ex.getTransactionId(), ex.isRetryable(), ex);
        HttpStatus status = ex.isRetryable()
            ? HttpStatus.SERVICE_UNAVAILABLE
            : HttpStatus.BAD_GATEWAY;
        return ResponseEntity.status(status)
            .body(ErrorResponse.of("PAYMENT_FAILED", ex.getMessage()));
    }

    // โœ… Catch-all: Unexpected server errors โ†’ 500
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleUnexpected(Exception ex) {
        log.error("Unexpected error โ€” this should be investigated", ex);
        return ResponseEntity
            .status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(ErrorResponse.of("INTERNAL_ERROR",
                "An unexpected error occurred. Please try again."));
    }

    // Standard error response body
    public record ErrorResponse(
        String    errorCode,
        String    message,
        Instant   timestamp,
        Map<String, Object> details
    ) {
        static ErrorResponse of(String code, String message) {
            return new ErrorResponse(code, message, Instant.now(), Map.of());
        }
        static ErrorResponse of(String code, String message,
                                Map<String, Object> details) {
            return new ErrorResponse(code, message, Instant.now(), details);
        }
    }
}
โ˜• JavaPaymentService.java โ€” Service Layer Exception Handling
package com.techsustainify.payment.service;

import java.util.Objects;

public class PaymentService {

    private final AccountRepository accountRepo;
    private final PaymentGatewayClient gatewayClient;
    private final TransactionRepository txRepo;

    /**
     * Processes a payment with full exception handling:
     * - Precondition validation (unchecked)
     * - Business rule enforcement (domain exception)
     * - Infrastructure failure translation (exception wrapping)
     * - Resource management (try-with-resources)
     *
     * @throws IllegalArgumentException  if request is invalid (programming error)
     * @throws ResourceNotFoundException if account not found (domain error)
     * @throws InsufficientFundsException if balance too low (domain error)
     * @throws PaymentGatewayException   if external gateway fails (infrastructure)
     */
    public TransactionResult processPayment(PaymentRequest request) {

        // โœ… Precondition checks โ€” fast fail with clear messages
        Objects.requireNonNull(request, "PaymentRequest must not be null");
        Objects.requireNonNull(request.getAccountId(), "Account ID required");
        if (request.getAmount() <= 0) {
            throw new IllegalArgumentException(
                "Payment amount must be positive. Received: " + request.getAmount());
        }

        // โœ… Load resource โ€” throws domain exception if not found
        Account account = accountRepo.findById(request.getAccountId())
            .orElseThrow(() -> ResourceNotFoundException.forAccount(
                request.getAccountId()));

        // โœ… Business rule โ€” throws domain exception with context
        if (account.getBalance() < request.getAmount()) {
            throw new InsufficientFundsException(
                account.getId(),
                account.getBalance(),
                request.getAmount()
            );
        }

        // โœ… Infrastructure call โ€” wrap checked exceptions in domain exception
        GatewayResponse gatewayResponse;
        try {
            gatewayResponse = gatewayClient.charge(
                account.getPaymentToken(),
                request.getAmount(),
                request.getCurrency()
            );
        } catch (GatewayTimeoutException e) {
            throw new PaymentGatewayException(
                e.getCode(), request.getIdempotencyKey(),
                true, // retryable โ€” timeout may be transient
                "Payment gateway timed out", e);
        } catch (GatewayRejectionException e) {
            throw new PaymentGatewayException(
                e.getCode(), request.getIdempotencyKey(),
                false, // not retryable โ€” card rejected
                "Payment rejected by gateway: " + e.getReason(), e);
        }

        // โœ… Persist transaction โ€” catch and wrap SQL exception
        try {
            Transaction tx = Transaction.builder()
                .accountId(account.getId())
                .amount(request.getAmount())
                .gatewayRef(gatewayResponse.getTransactionId())
                .build();
            txRepo.save(tx);
            account.deductBalance(request.getAmount());
            accountRepo.save(account);
            return TransactionResult.success(tx.getId(), gatewayResponse);
        } catch (Exception e) {
            // Gateway charged but DB failed โ€” log for manual reconciliation
            log.error("CRITICAL: Payment charged by gateway ({}) but not persisted!",
                gatewayResponse.getTransactionId(), e);
            throw new PaymentReconciliationException(
                gatewayResponse.getTransactionId(),
                "Payment processed externally but internal record failed", e);
        }
    }
}

Exception Flow Diagram โ€” What Happens When an Exception is Thrown

Understanding the JVM's exception propagation flow prevents the most common exception-handling bugs.

๐Ÿ’ฅ Exception thrown in try blockthrow new X() or JVM throws
Search handlers
๐Ÿ” Scan catch blocks top-to-bottomCheck each for type match
Evaluate type
โœ… Matching catch block found?instanceof check
YES โ€” match found
๐Ÿ”ง Execute matching catch blockException object available as 'e'
Catch done
๐Ÿงน Execute finally block (if present)Always runs regardless
Normal exit
๐Ÿ”ผ Propagate to caller's try-catchMove up the call stack
Run finally first
๐Ÿ“ญ Any caller has a match?Walk entire call stack
YES โ€” handled
๐Ÿ’€ Default handler โ€” print stack traceThread terminates
โžก๏ธ Continue after try-catchNormal execution resumes

Code Execution Flow โ€” from source to output

Java Try-Catch Interview Questions โ€” Beginner to Advanced

These questions appear in Java developer interviews from fresher to senior level, in OCPJP certification exams, and in technical design discussions about error handling strategy.

Practice Questions โ€” Test Your Exception Handling Knowledge

Attempt each question independently before reading the answer. These questions test real-world exception-handling reasoning โ€” the exact scenarios encountered in Java interviews and code reviews.

1. What is the output? try { System.out.println("1"); int x = 5 / 0; System.out.println("2"); } catch (ArithmeticException e) { System.out.println("3"); } catch (Exception e) { System.out.println("4"); } finally { System.out.println("5"); } System.out.println("6");

Easy

2. What is wrong with this code? Fix it. public int parseAge(String input) { try { return Integer.parseInt(input); } catch (Exception e) { return -1; } }

Easy

3. Will this compile? Explain why or why not. try { FileReader fr = new FileReader("data.txt"); } catch (FileNotFoundException e) { System.out.println("File not found"); } catch (IOException e) { System.out.println("IO error"); }

Easy

4. Refactor this legacy code to use try-with-resources. BufferedReader br = null; try { br = new BufferedReader(new FileReader(path)); return br.readLine(); } catch (IOException e) { throw new RuntimeException("Read failed", e); } finally { if (br != null) try { br.close(); } catch (IOException ignore) {} }

Medium

5. Design a custom exception class for a banking application where a transfer fails because the source account is frozen. What fields should it have?

Medium

6. What is the output and why? public static int getValue() { try { return 10; } finally { return 20; } } System.out.println(getValue());

Hard

7. What is wrong with this exception-handling pattern in a Spring service? @Service public class OrderService { public void processOrder(Order o) { try { orderRepo.save(o); } catch (Exception e) { log.error("Error saving order", e); throw e; } } }

Hard

8. Write a method that reads JSON from a URL, parses it, and returns the result โ€” with proper exception handling at each layer.

Hard

Conclusion โ€” Exception Handling: The Difference Between Brittle and Robust Code

Exception handling is where the quality gap between junior and senior Java developers is most visible. Junior code swallows exceptions, returns null from catch blocks, catches Exception broadly, and logs-then-rethrows at every layer. Senior code uses specific exceptions, carries domain context in custom exception fields, chains causes for full stack visibility, logs exactly once at the boundary, and uses try-with-resources for all resource management.

The most important insight about exception handling is that it is ultimately about communication โ€” between the code that detects a problem and the code (or human) that needs to respond. A catch(Exception e){} block says 'something went wrong but I'm hiding it from everyone.' A PaymentGatewayException with isRetryable(), getGatewayErrorCode(), and a chained cause says exactly what failed, why, and what to do about it.

ScenarioJunior ApproachSenior Approach
Resource cleanupManual finally + nested try-catchtry-with-resources (AutoCloseable)
Catching exceptionscatch (Exception e) { }catch (SpecificException e) with logging + rethrow
Method failsreturn nullthrow new DomainException with context
Checked exception in APIDeclare throws IOException everywhereWrap in unchecked domain exception at boundary
Wrapping exceptionsnew RuntimeException('failed', null)new DomainException('context', originalCause)
Multiple exceptionsDuplicate catch blocksMulti-catch: catch (A | B | C e)
Logginglog + rethrow at every layerLog ONCE at the outermost handling boundary
Custom exceptionsNo context โ€” just a message stringTyped fields (accountId, amount, errorCode)
Error vs Exceptioncatch (Throwable e) and continueNever catch Error; catch Exception at boundary only

Your next step: Java Collections Framework โ€” where you'll see exception handling in action as many collection operations (like NoSuchElementException from iterators and ConcurrentModificationException from unsafe iteration) require proper exception awareness. Understanding exceptions deeply makes every other Java topic more robust. โ˜•

Frequently Asked Questions โ€” Java Try-Catch