Java try-catch-finally — Handling Exceptions

4 minute read

The try-catch-finally statement is Java’s primary mechanism for handling exceptions. It lets you attempt risky code, catch failures, and clean up resources — regardless of what happens.


Basic Syntax

try {
    // Code that might throw an exception
} catch (ExceptionType e) {
    // Handle the exception
} finally {
    // Always executes — cleanup code
}

The try Block

The try block wraps code that might throw an exception:

try {
    int result = 10 / 0;  // throws ArithmeticException
    System.out.println(result);  // never reached
}

Key rules:

  • Variables declared inside try are scoped to that block — not visible in catch or finally.
  • If an exception occurs, execution jumps immediately to the matching catch block.
  • Code after the throwing line inside try is skipped.

The catch Block

Catches a specific exception type and provides a reference to the exception object:

try {
    int result = 10 / 0;
} catch (ArithmeticException e) {
    System.out.println("Error: " + e.getMessage());
    // Error: / by zero
}

What You Can Do with the Exception Object

catch (ArithmeticException e) {
    e.getMessage()          // "/ by zero"
    e.getClass().getName()  // "java.lang.ArithmeticException"
    e.printStackTrace()     // full stack trace to stderr
    e.getStackTrace()       // StackTraceElement[] array
}

The finally Block

finally always executes — whether an exception occurred or not, even after a return:

public static int divide(int a, int b) {
    try {
        return a / b;
    } catch (ArithmeticException e) {
        System.out.println("Cannot divide by zero");
        return -1;
    } finally {
        System.out.println("Finally block runs!");
    }
}

divide(10, 2);
// Output:
// Finally block runs!
// Returns: 5

divide(10, 0);
// Output:
// Cannot divide by zero
// Finally block runs!
// Returns: -1

When Does finally Run?

Scenario finally Runs?
No exception ✅
Exception caught ✅
Exception NOT caught ✅ (before propagation)
return in try ✅ (before return)
return in catch ✅ (before return)
System.exit() ❌
JVM crash ❌
Infinite loop in try ❌ (never reaches finally)

finally for Resource Cleanup

The classic use case — closing resources:

FileReader reader = null;
try {
    reader = new FileReader("data.txt");
    // read from file
} catch (IOException e) {
    System.out.println("Error reading file: " + e.getMessage());
} finally {
    if (reader != null) {
        try {
            reader.close();
        } catch (IOException e) {
            System.out.println("Error closing file");
        }
    }
}

This is verbose and error-prone. Java 7’s try-with-resources (covered later) is the modern replacement.


Multiple catch Blocks

Handle different exception types differently:

try {
    String[] arr = {"10", "abc", null};
    int index = 1;
    int value = Integer.parseInt(arr[index]);
    System.out.println(value);

} catch (NumberFormatException e) {
    System.out.println("Not a valid number: " + e.getMessage());

} catch (ArrayIndexOutOfBoundsException e) {
    System.out.println("Index out of range: " + e.getMessage());

} catch (NullPointerException e) {
    System.out.println("Null element encountered");
}

Order Matters!

Catch the most specific exception first:

// ✅ CORRECT — specific first
catch (FileNotFoundException e) { ... }
catch (IOException e) { ... }
catch (Exception e) { ... }

// ❌ WRONG — parent catches everything, child is unreachable
catch (Exception e) { ... }
catch (IOException e) { ... }   // compile error: unreachable

Multi-catch (Java 7+)

Handle multiple unrelated exceptions with one block:

try {
    // risky code
} catch (IOException | SQLException e) {
    System.out.println("Data error: " + e.getMessage());
    // e is implicitly final — you cannot reassign it
}

Rules:

  • Exception types must not be related (no parent-child).
  • The variable e is effectively final.

try-catch Without finally

finally is optional:

try {
    int n = Integer.parseInt("abc");
} catch (NumberFormatException e) {
    System.out.println("Invalid number");
}

try-finally Without catch

Also valid — ensures cleanup even if you don’t handle the exception:

Lock lock = new ReentrantLock();
lock.lock();
try {
    // critical section
} finally {
    lock.unlock();  // always release the lock
}

Nested try-catch

try {
    try {
        int result = 10 / 0;
    } catch (ArithmeticException e) {
        System.out.println("Inner catch: " + e.getMessage());
        throw new RuntimeException("Wrapped", e);  // rethrow
    }
} catch (RuntimeException e) {
    System.out.println("Outer catch: " + e.getMessage());
}

Avoid deep nesting — extract into methods instead.


What Happens When finally Throws?

try {
    throw new RuntimeException("from try");
} finally {
    throw new RuntimeException("from finally");  // replaces the try exception!
}
// Only "from finally" propagates — the original exception is LOST

Warning: Never throw from finally. It masks the original exception.


Execution Flow Visualised

try {
    statement 1  ──→ OK ──→ statement 2  ──→ OK ──→ statement 3
         │                       │
      exception               exception
         │                       │
         ▼                       ▼
    matching catch?         matching catch?
      yes → catch block       yes → catch block
      no  → propagate up      no  → propagate up
         │                       │
         └───────┬───────────────┘
                 ▼
           finally block (always)
                 │
                 ▼
           next statement (if exception was handled)

Common Mistakes

Mistake Problem Fix
Empty catch block Exception silently swallowed Log or rethrow
Catching Exception Too broad, hides bugs Catch specific types
Logic in finally May mask exceptions Keep cleanup only
Variable scope try-scoped vars invisible in catch Declare before try
Returning in finally Overrides try/catch return Avoid returns in finally
// BAD — swallowed exception
catch (Exception e) { }

// GOOD — at minimum, log it
catch (IOException e) {
    logger.error("Failed to read config", e);
}

Key Takeaways

  1. try wraps risky code; catch handles specific exceptions; finally always runs.
  2. Catch specific exceptions, ordered from most specific to most general.
  3. finally is for cleanup — don’t put business logic or return statements there.
  4. Use multi-catch (IOException | SQLException) for unrelated exceptions with the same handling.
  5. Never write an empty catch block — at minimum, log the exception.

What’s Next?

Next: throw and throws — creating and declaring exceptions, rethrowing, and exception chaining.

Updated: