Mastering Java Exception Handling, Part 2: Checked vs. Unchecked Exceptions, 'throws' Keyword, and Finally Block

"This article is the second part of the series Mastering Java Exception Handling. In Part 1, we covered how Java spots issues at compile-time and runtime, introducing try-catch for problems like ArithmeticException. Missed it? Catch up here. Now, in Part 2, we’re diving into checked vs. unchecked exceptions, the throws keyword, and the finally block—plus a few handy nuances."

Checked vs. Unchecked Exceptions

"From Part 1, we know Throwable splits into Error and Exception at runtime. Error and RuntimeException (a subclass of Exception) plus their subclasses are unchecked—no need to catch or declare them. Everything else under Exception (and Throwable itself) is checked, meaning Java insists you catch them or declare them with throws in the method signature.

But why this split? Java marks Error and RuntimeException as unchecked because they’re usually programming mistakes or system failures—like NullPointerException (oops, forgot to check null!) or OutOfMemoryError (JVM ran out of space). These are hard to predict or fix on the fly, so Java trusts you’ll sort the code or system instead. Checked exceptions (like IOException) are expected issues—things that can happen even in good code, like a missing file. Java forces you to plan for these, keeping your program solid.

Here’s a checked exception in action:"

private static void checkedExample(boolean x) {
    try {
        if (x) {
            System.out.println("All good");
        } else {
            throw new Exception("Oops!!");
        }
    } catch (RuntimeException e) {
        System.out.println("RuntimeException caught!");
    }
}

"Output: java: unreported exception java.lang.Exception; must be caught or declared to be thrown. The checked Exception isn’t caught by RuntimeException (a narrower type), so you need:"

private static void checkedExampleDeclared(boolean x) throws Exception {
    throw new Exception();
}
// OR
private static void checkedExampleCaught(boolean x) {
    try {
        throw new Exception();
    } catch (Exception e) {
        System.out.println("Caught: " + e.getMessage());
    }
}

"Unchecked example? Simple:"

private static void uncheckedExample() {
    throw new RuntimeException();
}

"This compiles fine—no throws needed."

Error: The Unchecked Trouble

"Error, another Throwable subclass, is unchecked and signals serious issues like OutOfMemoryError (out of memory) or StackOverflowError (stack overflow from recursion). These aren’t typical bugs but JVM or system failures.

Example:"

public class ErrorDemo {
    public static void main(String[] args) {
        try {
            int[] hugeArray = new int[Integer.MAX_VALUE];
        } catch (OutOfMemoryError e) {
            System.out.println("Caught: " + e.getMessage());
        }
    }
}

"Output (if it runs): Caught: Java heap space. Don’t rely on catching errors—log them and exit gracefully:"

try {
    recursiveInfiniteLoop();
} catch (StackOverflowError e) {
    System.err.println("Fatal: " + e.getMessage());
    System.exit(1);
}

"Best practice? Avoid catching Error unless logging; optimize code to prevent them."

Static Type Checking

private static void staticTypeCheckingExample() {
    try {
        if (true) {
            System.out.println("All good");
        } else {
            throw new Exception("Oops!");
        }
    } catch (RuntimeException e) {
        System.out.println("RuntimeException caught!");
    }
}

"Output: java: unreported exception java.lang.Exception.

By reading the above code and its output, you might think that the code inside the else condition will never be executed and the ‘Exception‘ will never be thrown, then why does java throw the compilation error stating it must be caught or declared thrown? the answer is static type checking. Java’s compiler doesn’t perform deep control-flow analysis to deduce that only an unchecked RuntimeException will be thrown. It uses static type checking based on the written code, not runtime behavior or branch reachability."

Exception Wrapping

public static void main(String[] args) {
    try {
        checked(); // Calls method throwing checked exception
    } catch (FileNotFoundException e) {
        System.out.println("Caught checked exception: " + e.getMessage());
        throw new RuntimeException("File operation failed", e); // Wraps, rethrows
    }
}
How It Works
  • Catching Checked Exception: main’s try-catch catches FileNotFoundException from checked(). Satisfies compiler’s rule.

  • Rethrowing as RuntimeException: Throws new RuntimeException, passing original exception (e) as cause. Being unchecked, needs no throws.

  • Propagation: RuntimeException travels up call stack, crashing program unless caught higher up.

Why Do This?
  • Simplifying Callers: Wrapping frees callers from handling specific checked exceptions (e.g., FileNotFoundException) they can’t fix.

  • Layered Design: Lower-level code throws checked exceptions (like IOException), higher-level prefers unchecked for broader signals.

  • Logging or Cleanup: Catch to log or clean up, then rethrow as unchecked.

The Finally Block: Cleanup Guaranteed

In Java, the finally block is the trusty sidekick to try and catch, ensuring that certain code runs no matter what—whether an exception is thrown or not. It’s your go-to for cleanup tasks, like closing files, releasing resources, or resetting states, making your program more reliable.

How It Works

The finally block comes after try and optional catch blocks. It executes:

  • After the try block completes successfully (no exception).

  • After a catch block handles an exception.

  • Even if a return, break, or continue tries to exit early.

Think of it as the “always happens” clause in your exception-handling story.

A Quick Example

Here’s a simple case where finally shines:

public class FinallyDemo {
    public static void main(String[] args) {
        java.io.PrintWriter writer = null;
        try {
            writer = new java.io.PrintWriter("output.txt");
            writer.println("Hello, Java!");
            int result = 10 / 0; // Throws ArithmeticException
        } catch (ArithmeticException e) {
            System.out.println("Caught: " + e.getMessage());
        } finally {
            System.out.println("Running finally...");
            if (writer != null) {
                writer.close(); // Ensures the file is closed
            }
        }
    }
}

Output:

Caught: / by zero
Running finally...

Here, the try block opens a file and then throws an ArithmeticException. The catch block handles it, but finally ensures the writer is closed—no resource leaks, even with an error.

Why Use It?

  • Resource Management: Perfect for closing files, database connections, or network sockets.

  • Guaranteed Execution: Runs even if an exception isn’t caught or a method returns early.

  • Code Clarity: Keeps cleanup logic separate from error handling.

Nuances to Watch

  1. Executes Almost Always: The only way finally skips is if the JVM exits (e.g., System.exit(0)) or the program crashes hard (e.g., power failure). Otherwise, it’s unstoppable.

  2. Return Overrides: If try or catch has a return, finally still runs before the return, and it can overwrite the return value:

     public static int trickyFinally() {
         try {
             return 1;
         } finally {
             return 2; // Overrides the 1
         }
     }
    

    Output: 2. Avoid this—it’s confusing and a bad practice.

  3. Exceptions in Finally: If finally throws an exception, it overrides any prior exception from try or catch, masking the original issue. Be cautious with risky operations here.

  4. Try-With-Resources Alternative: Since Java 7, try-with-resources can replace finally for resource cleanup (e.g., try (PrintWriter writer = new PrintWriter("file.txt"))). We’ll explore that later, but finally remains versatile for non-resource tasks.

When to Use It

Use finally when you need guaranteed cleanup or final steps, but lean on try-with-resources for modern resource management. It’s a classic tool that ensures your code stays tidy, exception or not.

Exceptions in Catch or Finally

"What if catch or finally throws an exception? It can override the original issue.

Catch Example
try {
    int result = 10 / 0;
} catch (ArithmeticException e) {
    System.out.println("Caught: " + e.getMessage());
    String text = null;
    text.length(); // NullPointerException
}

"Output: Crashes with NullPointerException. Fix with nesting:"

try {
    int result = 10 / 0;
} catch (ArithmeticException e) {
    try {
        String text = null;
        text.length();
    } catch (NullPointerException npe) {
        System.out.println("Caught in catch: " + npe.getMessage());
    }
}

"Finally Example: Same fix—nest a try-catch."

Best Practices for Exception Handling

"Exception handling is like a safety net—weaver it wrong, and your code’s a mess. Here are some best practices to keep it clean:

  • Catch Specific Exceptions First: Catch precise ones—like ArrayIndexOutOfBoundsException—before Exception. If Exception comes first, it hides specifics.

  • Avoid Catching Throwable or Exception Broadly: Overkill—might grab Error or unrelated issues. Stick to specifics unless logging a mystery.

  • Use finally for Cleanup: Ensures resources like FileWriter close, exception or not. Keep it simple to avoid new exceptions.

  • Declare Checked Exceptions with throws Wisely: Only use throws for unhandled checked exceptions (like IOException). Don’t burden callers unnecessarily.

  • Create Custom Exceptions When Needed: For app-specific rules (e.g., InvalidUserInputException), extend RuntimeException or Exception. Clear and tailor-made.

  • Log Exceptions Effectively: Log details—file names, inputs—with stack trace: log.error("File failed: " + fileName, e)."