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 catchesFileNotFoundException
fromchecked()
. Satisfies compiler’s rule.Rethrowing as RuntimeException: Throws new
RuntimeException
, passing original exception (e
) as cause. Being unchecked, needs nothrows
.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
, orcontinue
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
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.Return Overrides: If
try
orcatch
has areturn
,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.Exceptions in Finally: If
finally
throws an exception, it overrides any prior exception fromtry
orcatch
, masking the original issue. Be cautious with risky operations here.Try-With-Resources Alternative: Since Java 7,
try-with-resources
can replacefinally
for resource cleanup (e.g.,try (PrintWriter writer = new PrintWriter("file.txt"))
). We’ll explore that later, butfinally
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
—beforeException
. IfException
comes first, it hides specifics.Avoid Catching
Throwable
orException
Broadly: Overkill—might grabError
or unrelated issues. Stick to specifics unless logging a mystery.Use
finally
for Cleanup: Ensures resources likeFileWriter
close, exception or not. Keep it simple to avoid new exceptions.Declare Checked Exceptions with
throws
Wisely: Only usethrows
for unhandled checked exceptions (likeIOException
). Don’t burden callers unnecessarily.Create Custom Exceptions When Needed: For app-specific rules (e.g.,
InvalidUserInputException
), extendRuntimeException
orException
. Clear and tailor-made.Log Exceptions Effectively: Log details—file names, inputs—with stack trace:
log.error("File failed: " + fileName, e)
."