Robust Oop

Exceptions

Exceptions keep error handling apart from normal logic. Java splits them into checked exceptions, which the compiler forces you to handle or declare (recoverable conditions), and unchecked ones (RuntimeException, for programming bugs). try/catch recovers, several catch clauses or one multi-catch handle different types, throw raises, throws declares, and finally always cleans up. try-with-resources closes resources automatically in reverse order and keeps close failures as suppressed exceptions. Also covers the hierarchy, stack traces, custom exceptions and the return-in-finally trap, all with verified output.

Suggest an edit

Exceptions — Handling What Goes Wrong

You've already watched programs crash: a NullPointerException, an ArithmeticException, a bad parse. Now it's time to take control of them.

  • An exception is Java's way of signaling "something went wrong". It transfers control to code that can deal with the problem, instead of returning error codes that callers forget to check.
  • The defining design choice is a split. Checked exceptions are recoverable conditions that the compiler forces you to handle or declare. Unchecked ones (RuntimeException) signal programming bugs, and carry no such requirement.
  • Around that split, you get try/catch to recover, throw/throws to raise and declare, finally to always clean up, and try-with-resources to close things automatically.

💡 The core idea.

  • An exception signals "something went wrong" and transfers control to code that can handle it.
  • The defining split: checked (the compiler forces you to handle or declare them) vs unchecked (RuntimeException, for bugs).
  • Around that split sit try/catch, throw/throws, finally, and try-with-resources.

Every output below was produced by compiling and running the code on Java 25.

You'll be able to: trace which statements run when a try block throws, and read from a stack trace which method threw; predict whether a call compiles based on whether its exception is checked, and fix unreported exception with catch or throws; write a custom exception, and catch two unrelated exception types in a single multi-catch clause; predict the order of opening, closing and suppressed exceptions in try-with-resources; explain why a return inside finally loses an exception.

📘 How to read the Intuition boxes. Each one is built in three moves:

  1. The mechanism — what the compiler and the JVM do.
  2. A concrete bite — a specific, runnable failure (often a real compiler error), shown so the trap is visible.
  3. The earned rule — the decision heuristic, now justified rather than asserted, plus its cost.

Table of contents

  1. try/catch: recover instead of crash
  2. The hierarchy: checked vs unchecked
  3. Several catch clauses, and multi-catch
  4. throw, throws, and custom exceptions
  5. finally: cleanup that always runs
  6. try-with-resources
  7. Mental-model summary
  8. Gotcha checklist
  9. Check yourself
  10. Sources

1. try/catch: recover instead of crash

Wrap risky code in a try. If it throws, control jumps to a matching catch, which handles the problem — so the program keeps going instead of dying.

Output:

parsed: 42
bad input: oops
parsed: 7
done

Analysis. parseInt("oops") threw a NumberFormatException. Back in Input & Output, that crashed the program. This time, the catch handled it: it printed bad input: oops, and the loop carried on to "7".

  • The exception transferred control from the failing parseInt straight to the catch.
  • It skipped the println("parsed: ...") for that iteration.
  • One bad input no longer sinks the whole program.

Intuition. Mechanism. When code in a try throws, the JVM abandons the rest of the try block. It looks for a catch whose type matches the exception (here, NumberFormatException). If one matches, its body runs, and execution continues after the whole try/catch. The thrown object (e) carries the details: a message and a stack trace.

Concrete bite. The win here is targeted recovery. You guard only the operations that can fail, and the handler decides what "recover" means (skip, retry, use a default). Catch too broadly (catch (Exception e)), and you may swallow bugs you need to see. Catch the specific type, and unexpected exceptions still propagate.

💡 Earned rule. Wrap exactly the operation that can fail in a try, and catch the specific exception you can handle.

  • The cost is extra structure, plus the temptation to over-catch.
  • The payoff: anticipated failures become recoverable events instead of crashes. The catch's scope even documents which line was expected to fail.

2. The hierarchy: checked vs unchecked

Every exception is a Throwable, and the tree splits into Error and Exception [1]:

  • An Error indicates "serious problems that a reasonable application should not try to catch" [10], such as running out of memory.
  • Within Exception, the subclass RuntimeException and all its descendants are unchecked, just like every Error.
  • Everything else under Exception is checked.

The difference comes down to one compiler rule: checked exceptions must be caught or declared.

An unchecked exception (a RuntimeException such as ArithmeticException) needs no declaration. The code compiles, and fails only at run time, if and when it throws:

Output (prints 5, then crashes):

5
Exception in thread "main" java.lang.ArithmeticException: / by zero

Analysis. divide can throw ArithmeticException. Since that's an unchecked exception, it needed no throws and no try. The code compiled without complaint, ran divide(10, 2) just fine (5), and then threw on divide(10, 0). Unchecked exceptions represent bugs (a zero divisor, a null dereference, a bad index) that the compiler doesn't force you to anticipate everywhere.

Intuition. Mechanism. The compiler keeps track of checked exceptions: any code that can throw one must either catch it or declare it with throws [2]. RuntimeExceptions and Errors are exempt. They could happen almost anywhere, so requiring declarations for them would be unbearable.

Concrete bite. Call something that throws a checked exception without handling it, and the compiler refuses:

Compiler error:

Main.java:8: error: unreported exception IOException; must be caught or declared to be thrown
        risky();
             ^

IOException is checked, so calling risky() without a try/catch (or a throws on main) won't compile. Wrapping the call fixes it: try { risky(); } catch (IOException e) { … }.

The rule cuts the other way, too. A catch for a checked exception that its try block can never throw is also an error [3]. For example, catch (IOException e) around a plain println gives exception IOException is never thrown in body of corresponding try statement.

💡 Earned rule. Use checked exceptions for recoverable, expected conditions that a caller should consciously handle (a missing file, a network timeout). Use unchecked ones (RuntimeException) for programming errors (null, a bad argument, a bad index): those are bugs to fix, not cases to handle.

  • The cost is the famous friction of checked exceptions: they propagate up through every signature.
  • The payoff is that the compiler guarantees anticipated failures aren't silently ignored.

3. Several catch clauses, and multi-catch

A try can have several catch clauses. The JVM tries them in order, from top to bottom, and runs the first one whose type matches. When two exceptions need the same handling, a single multi-catch clause can list both types, separated by | [4]:

Output:

value: 10
skipped 1: NumberFormatException
skipped 2: ArrayIndexOutOfBoundsException

Analysis. Index 1 held "ten", so parseInt threw a NumberFormatException. Index 2 is past the end of the two-element array, so parts[i] threw an ArrayIndexOutOfBoundsException. One clause caught both, and e.getClass() reported which one had arrived.

Intuition. Mechanism. In a multi-catch, e has the closest common supertype of the listed types, so you can call only the methods they share. e is also implicitly final, which means the handler can't assign to it [4].

Concrete bite. The alternatives may not be related by subclassing [4]. NumberFormatException extends IllegalArgumentException, so listing both is redundant — and javac tells you so:

Compiler error:

Main.java:5: error: Alternatives in a multi-catch statement cannot be related by subclassing
        } catch (NumberFormatException | IllegalArgumentException e) {
                                         ^
  Alternative NumberFormatException is a subclass of alternative IllegalArgumentException
1 error

Keep only IllegalArgumentException, since it already catches its subclass.

Non-example: a general catch before a specific one. Clauses are tried in order. Put catch (Exception e) first, and it catches everything, so a later catch (NumberFormatException e) could never run. That's a compile-time error [3]:

Compiler error:

Main.java:7: error: exception NumberFormatException has already been caught
        } catch (NumberFormatException e) {
          ^
1 error

The fix: put the specific clause first, and the general one last.

💡 Earned rule. Order catch clauses from specific to general. When two unrelated exceptions need the same handling, write one multi-catch clause instead of two identical bodies.

  • The cost: inside a multi-catch, e offers only what the types share, and you can't reassign it.
  • The payoff: one handler, no duplicated code, and a compiler that rejects clauses that could never run.

4. throw, throws, and custom exceptions

Three tools let you raise failures and give them names:

  • throw raises an exception [8].
  • throws declares that a method may raise a checked exception [9].
  • Your own exception class extends either Exception (checked) or RuntimeException (unchecked).

A custom type gives the failure a name, and carries its data along.

Output:

balance: 50
denied: need 100, have 50

Analysis.

  • withdraw(50) succeeded (leaving a balance of 50).
  • withdraw(100) saw that amount > balance and threw an InsufficientFundsException. The exception unwound out of withdraw to the catch, which printed the message it carried.
  • InsufficientFundsException extends Exception, so it's checked. That's why withdraw declares throws InsufficientFundsException, and why the caller must handle it.

The custom type turns the failure into a named, catchable thing with its own data.

Intuition. Mechanism. throw stops the current method and propagates the exception up the call stack. Each caller in turn is abandoned, until a matching catch turns up. If none does, the thread ends, and by default a stack trace is printed: first the exception, then one at line per method it passed through, innermost first. throws declares that propagation in the method's signature.

In this example, no method catches the exception, so the trace shows the whole path:

Output (prints 1030, then a thrown exception):

1030
Exception in thread "main" java.lang.IllegalArgumentException: negative age: -5
	at Main.checkAge(Main.java:4)
	at Main.register(Main.java:9)
	at Main.main(Main.java:13)

Read it from the top down. The first at line is where the exception was thrown: checkAge, line 4. Each line below it is a caller that was waiting: register at line 9, then main at line 13. When you're debugging, the first line under the message is the one to open first.

Concrete bite. A custom exception beats a generic one (throw new Exception("...")). Callers can catch (InsufficientFundsException e) specifically, and tell it apart from other failures. The type itself documents the condition, and the message (super(msg)) and any extra fields travel with it to the handler.

💡 Earned rule. throw a specific exception type (a custom one when no standard type fits), and declare checked ones with throws. Extend Exception for conditions callers should handle, and RuntimeException for misuse and bugs.

  • The cost is a class per condition, plus the throws plumbing.
  • The payoff: failures that are named, catchable by type, and self-documenting — far better than error codes or a bare boolean return.

5. finally: cleanup that always runs

A finally block attached to a try runs no matter how the try exits — normal completion, a caught exception, or even a return. That makes it the place for cleanup that must always happen.

Output:

finally ran
from try

Analysis. Look closely at the order. process() hit return "from try", but the finally ran before the method actually returned. So finally ran printed first, followed by the returned value.

finally intercepts every exit path, including a return in the middle of the try. That's what makes it reliable for cleanup.

Intuition. Mechanism. Whatever ends the try — completion, an exception, or a return — the finally block runs on the way out [5]. For a return, the value is computed first, then finally runs, and then control leaves. The only ways to skip it are for the try to never end, or for the JVM to stop first — and System.exit never returns [11].

Output:

in try

The JVM stopped inside the try, so there's no finally ran.

Concrete bite. The surprise is that finally ran comes before from try, because people expect a return to leave immediately. It doesn't. finally always gets its turn first — and that's the whole point. You can release the lock or close the file, even if you bailed out early.

Non-example: a return inside finally. If finally itself ends abruptly, through a return or a throw, that outcome replaces the try's, and the original exception is discarded [5]:

Output:

from finally

The IllegalStateException simply vanished: no crash, no trace, just an ordinary return value. javac compiles this without a word. Only if you pass the -Xlint:finally option does it warn you: finally clause cannot complete normally.

💡 Earned rule. Put must-always-run cleanup in finally, and never return (or throw) from inside it.

  • The cost: finally runs on every path, so a misused one can mask the try's outcome.
  • The payoff: cleanup that's guaranteed no matter how the block exits. For resources, the next section's tool is even cleaner.

6. try-with-resources

Closing resources by hand in finally is verbose and easy to get wrong. With try-with-resources, you declare the resource in parentheses right after try. Any object that implements AutoCloseable is then closed automatically when the block exits, in the reverse order of opening [6].

Output:

open file
use file
close file
after

Analysis. The resource opened and got used. Then close() ran automatically as the try block exited — before after printed, and with no explicit finally. Had use() thrown, close() would still have run. Think of try-with-resources as finally-based cleanup, specialized for anything AutoCloseable (files, streams, connections, locks), and generated correctly for you.

Intuition. Mechanism. The compiler expands try (R r = ...) into a try/finally that calls r.close() on exit [7]. It also handles the edge cases: it closes the resource even if the body throws. And if the body threw and close() throws too, the close() exception is suppressed: it gets attached to the body's exception, which is the one you catch [6]:

Output:

open file
close file
caught: body failed
suppressed: close of file failed

Analysis. The resource was closed before the catch ran. The body's body failed is the exception that arrived, so the real cause isn't lost. The close() failure rode along with it, and getSuppressed() [12] returned it.

Concrete bite. The alternative — a manual finally { r.close(); } — is where leaks hide. Forget it, get the ordering wrong, or let close() itself throw and mask the real error, and a file handle or connection leaks. try-with-resources removes that whole class of bug by construction. The close isn't optional, and it isn't yours to forget.

💡 Earned rule. Use try-with-resources for anything you must close — files, streams, sockets, connections, locks. Save a bare finally for cleanup that isn't an AutoCloseable.

  • The cost: your resource type must implement AutoCloseable.
  • The payoff: guaranteed, correctly ordered, exception-safe cleanup, with none of the manual finally boilerplate. I/O, Files & NIO.2 uses it for real file I/O.

7. Mental-model summary

Principle Consequence
try/catch transfers control to a handler on a throw An anticipated failure becomes recoverable, not a crash
Checked exceptions must be caught or declared; unchecked need not A checked call without handling won't compile; a RuntimeException only fails at run time
catch clauses are tried in order; multi-catch joins unrelated types with | Specific before general; related alternatives or an unreachable clause won't compile
throw raises, throws declares, custom types name the failure Callers can catch a specific type and read its data
An uncaught exception unwinds every caller The stack trace lists the thrower first, then each waiting caller
finally runs on every exit path, even a return Reliable cleanup — but a return/throw inside it discards the try's exception
try-with-resources auto-closes any AutoCloseable, last opened first Guaranteed, ordered close; a failing close() is kept as a suppressed exception

8. Gotcha checklist

Symptom Likely cause Fix
unreported exception IOException; must be caught or declared to be thrown a checked exception isn't handled wrap it in try/catch, or add throws to the method
exception IOException is never thrown in body of corresponding try statement a catch for a checked exception the try cannot throw remove the catch, or catch what the code does throw
exception NumberFormatException has already been caught a general catch sits above a specific one put the specific clause first
Alternatives in a multi-catch statement cannot be related by subclassing one listed type extends another keep only the supertype
An exception crashes despite a catch the catch type doesn't match the thrown one catch the actual type (the trace's first line names it)
Exception in thread "main" and a list of at lines an exception no method caught the first at line is where it was thrown; the first line naming your own code is where to look
Cleanup didn't run on an early return it was not in finally put it in finally, or use try-with-resources
An exception "disappeared" a return or throw inside finally replaced it never exit from finally; compile with -Xlint:finally to be warned
finally didn't run System.exit stopped the JVM inside the try don't call System.exit where cleanup is pending
A close() failure seems lost it was suppressed behind the body's exception read e.getSuppressed()
A file/stream/connection leaked it was closed by hand, or not at all use try-with-resources so close() is guaranteed

✅ Check yourself

One check per objective. Answer before you open anything.

The 🧪 box below: the {"1", "x", "3"} total, read() without a try, and two resources.

Output:

open A
open B
use both
close B
close A
skipped x
total: 4
  • B closes before A, because resources close in the reverse order of opening. A resource opened later may depend on an earlier one (think of a reader built on a file), so it gets closed while the earlier one still works.
  • "x" was skipped, so the total is 1 + 3 = 4.
  • Calling void read() throws IOException from main without a try doesn't compile: you get unreported exception IOException; must be caught or declared to be thrown. Add throws IOException to main, or wrap the call.

📚 Sources

  1. The Java Language Specification, Java SE 25, §11.1.1 "The Kinds of Exceptions" (the checked and unchecked exception classes) — https://docs.oracle.com/javase/specs/jls/se25/html/jls-11.html#jls-11.1.1
  2. The Java Language Specification, Java SE 25, §11.2 "Compile-Time Checking of Exceptions" — https://docs.oracle.com/javase/specs/jls/se25/html/jls-11.html#jls-11.2
  3. The Java Language Specification, Java SE 25, §11.2.3 "Exception Checking" (a catch the try cannot reach; a catch a preceding clause already covers) — https://docs.oracle.com/javase/specs/jls/se25/html/jls-11.html#jls-11.2.3
  4. The Java Language Specification, Java SE 25, §14.20 "The try statement" (multi-catch; alternatives may not be subtypes; the parameter is implicitly final) — https://docs.oracle.com/javase/specs/jls/se25/html/jls-14.html#jls-14.20
  5. The Java Language Specification, Java SE 25, §14.20.2 "Execution of try-finally and try-catch-finally" (an abrupt finally discards the earlier reason) — https://docs.oracle.com/javase/specs/jls/se25/html/jls-14.html#jls-14.20.2
  6. The Java Language Specification, Java SE 25, §14.20.3 "try-with-resources" ("closed in the reverse order"; a later exception "is suppressed") — https://docs.oracle.com/javase/specs/jls/se25/html/jls-14.html#jls-14.20.3
  7. The Java Language Specification, Java SE 25, §14.20.3.1 "Basic try-with-resources" (the translation to try/finally) — https://docs.oracle.com/javase/specs/jls/se25/html/jls-14.html#jls-14.20.3.1
  8. The Java Language Specification, Java SE 25, §14.18 "The throw Statement" — https://docs.oracle.com/javase/specs/jls/se25/html/jls-14.html#jls-14.18
  9. The Java Language Specification, Java SE 25, §8.4.6 "Method Throws" — https://docs.oracle.com/javase/specs/jls/se25/html/jls-8.html#jls-8.4.6
  10. java.lang.Error, Java SE 25 API ("serious problems that a reasonable application should not try to catch") — https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/Error.html
  11. java.lang.Runtime, Java SE 25 API (exit: "An invocation of this method never returns normally") — https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/Runtime.html
  12. java.lang.Throwable, Java SE 25 API (getSuppressed: exceptions "suppressed, typically by the try-with-resources statement") — https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/Throwable.html

🧪 Predict, then check.

  1. Predict the output of a loop that parseInts {"1", "x", "3"} inside a try/catch, sums the valid values, and prints the total.
  2. Predict whether you can call a method void read() throws IOException from main without a try, and what error you get if you can't.
  3. Predict the exact order of lines printed by a try-with-resources that opens two resources, A then B (each printing on open and on close). Explain why B closes before A.

Your Turn

Before you move on, check your understanding with the coach — explain the idea, apply it, weigh the trade-offs, then defend your reasoning.

Mark as readBook versionfd26460