Checked exceptions, unchecked exceptions, and try-with-resources
The one real difference between the two families, and the resource leak try-with-resources exists to prevent.
After this lesson you can
- Tell a checked exception from an unchecked one
- Say why the compiler forces you to handle one but not the other
- Order catch clauses so a subclass is never swallowed by its parent
- Use try-with-resources instead of a hand-written finally block
Every Throwable is either checked or unchecked, and the
difference is entirely about the compiler, not about how serious the
problem is.
public String readFile(String path) throws IOException { // checked: must declare
return Files.readString(Path.of(path));
}
public int divide(int a, int b) { // unchecked: no declaration needed
return a / b; // may throw ArithmeticException, uncaught
}
IOException extends Exception — checked. The compiler requires every
method that can throw one to either catch it or declare it with
throws, and every caller is forced to deal with that, one way or
another. ArithmeticException, NullPointerException, and
IllegalArgumentException all extend RuntimeException — unchecked.
Nothing forces a caller to acknowledge them; they propagate silently
until something up the stack catches them, or nothing does and the
thread dies with a stack trace. (Error and its subclasses, such as
OutOfMemoryError, are unchecked too.)
The convention: checked exceptions for conditions a well-written caller can reasonably recover from — a missing file, a failed network call. Unchecked exceptions for programmer errors — a null that should never have been null, an index that was never in range — where the fix is a code change, not a recovery path.
Try it
public class Solution { public static String describe() { try { int[] nums = {1, 2, 3}; return "value=" + nums[5]; } catch (ArrayIndexOutOfBoundsException e) { return "array: " + e.getMessage(); } catch (RuntimeException e) { return "runtime: " + e.getMessage(); } }}Catch the most specific type first
try {
process(data);
} catch (FileNotFoundException e) { // a subclass of IOException
// ...
} catch (IOException e) { // the broader type
// ...
}
A catch block matches the first clause whose type the thrown exception
is an instance of, top to bottom. Putting the broader IOException
first would catch everything there, and the compiler refuses to compile
a narrower catch clause that can now never be reached — the ordering
is enforced, not just a style preference. The hierarchy is not always
obvious: NumberFormatException is a subclass of
IllegalArgumentException, so a lone catch (IllegalArgumentException e) also catches every failed Integer.parseInt.
try-with-resources
try (BufferedReader reader = new BufferedReader(new FileReader(path))) {
return reader.readLine();
}
// reader.close() has already run here, even if readLine() threw
Anything implementing AutoCloseable — a file, a database connection, a
lock — declared inside the parentheses of a try is closed automatically
when the block exits, success or exception, in the reverse of the order
it was opened. The equivalent by hand is a finally block that has to
null-check the resource (it might have failed to open) and can itself
throw while closing, silently replacing the original exception. With
try-with-resources the body's exception wins, and an exception from
close() is attached to it as a exception
() instead of hiding it.
import java.util.*; public class Solution { static List<String> log = new ArrayList<>(); record Connection(String name, boolean closeFails) implements AutoCloseable { Connection { log.add("open " + name); } @Override public void close() { log.add("close " + name); if (closeFails) throw new IllegalStateException(name + " close failed"); } } public static List<String> describe() { log = new ArrayList<>(); try (Connection db = new Connection("db", false); Connection cache = new Connection("cache", true)) { log.add("working"); throw new IllegalArgumentException("query failed"); } catch (IllegalArgumentException e) { log.add("caught: " + e.getMessage()); for (Throwable s : e.getSuppressed()) log.add("suppressed: " + s.getMessage()); } return log; }}Common mistakes
catch (Exception e) {}around a large block. It swallows theNullPointerExceptionfrom your own bug along with the failure you meant to handle, and the program carries on with bad state.- Wrapping without the cause:
throw new ServiceException("load failed")loses the original stack trace. Pass it on —new ServiceException("load failed", e). - Returning (or throwing) from
finally. Areturninfinallyreplaces whatever thetryblock was doing, including an exception in flight, which then vanishes without a trace.
public class Solution { @SuppressWarnings("finally") static String load() { try { throw new IllegalStateException("database is down"); } finally { return "default config"; // discards the exception in flight } } public static String describe() { try { return "load() returned: " + load(); } catch (IllegalStateException e) { return "caller saw: " + e.getMessage(); } }}What interviewers ask
- Checked or unchecked — how do you decide? Checked when the caller can do something useful about it and should be made to think about it; unchecked for bugs and for failures nobody near the call site can fix. Many modern APIs lean unchecked because checked exceptions do not compose with lambdas.
- Does
finallyalways run? For normal completion,return,breakand exceptions, yes; not if the JVM halts first (System.exit, a crash) or thetrynever finishes. - What happens if both the body and
close()throw in try-with-resources? The body's exception propagates and the one fromclose()is added to it as suppressed.
Exercise · Pro
Tell a parse failure from a validation failure
Exercises are free in the first 3 lessons of every course. From here on they are part of Pro and Sprint; the lesson and its runnable examples stay open.
Upgrade