19. Exceptions and Error Handling
Table of Contents
- 19.1 Exception hierarchy and types
- 19.2 Declaring and throwing exceptions
- 19.3 Overriding methods and exception rules
- 19.4 Handling exceptions: try, catch, finally
- 19.5 Automatic Resource Management try-with-resources
- 19.6 Suppressed exceptions
- 19.7 Exceptions summary
Exceptions are Java’s structured mechanism for handling abnormal conditions at runtime.
They allow programs to separate normal execution flow from error-handling logic, improving robustness, readability, and correctness.
19.1 Exception hierarchy and types
All exceptions derive from Throwable.
The hierarchy defines which conditions are recoverable, which must be declared, and which represent fatal system failures.
java.lang.Object
└── java.lang.Throwable
├── java.lang.Error
└── java.lang.Exception
└── java.lang.RuntimeException
19.1.1 Throwable
- Base class for all errors and exceptions
- Supports message, cause, and stack trace
- Only
Throwable(and subclasses) can be thrown or caught
19.1.2 Error (unchecked)
- Represents serious JVM or system problems
- Not intended to be caught or handled
- Examples:
OutOfMemoryError,StackOverflowError
Note
Errors indicate conditions from which the application is generally not expected to recover.
19.1.3 Checked Exceptions (Exception)
- Subclasses of
ExceptionexcludingRuntimeException - Represent conditions that applications may want to handle
- Must be either caught or declared
- Examples:
IOException,SQLException
19.1.4 Unchecked Exceptions (RuntimeException)
- Subclasses of
RuntimeException - Not required to be declared or caught
- Usually represent programming errors
- Examples:
NullPointerException,IllegalArgumentException
19.2 Declaring and throwing exceptions
19.2.1 Declaring exceptions with throws
A method declares checked exceptions using the throws clause. This is part of the method’s API contract.
void readFile(Path p) throws IOException {
Files.readString(p);
}
Note
- Only checked exceptions must be declared.
- Unchecked exceptions may be declared, but are usually omitted.
19.2.2 Throwing exceptions
Exceptions are created with new and thrown explicitly using throw.
if (value < 0) {
throw new IllegalArgumentException("value must be >= 0");
}
throwthrows exactly one exception instancethrowsdeclares possible exceptions in the method signature
19.3 Overriding methods and exception rules
When overriding a method, exception rules are strictly enforced.
- An overriding method may throw fewer or narrower checked exceptions
- It may throw any unchecked exceptions
- It may throw no new or broader checked exceptions
class Parent {
void work() throws IOException {}
}
class Child extends Parent {
@Override
void work() throws FileNotFoundException {} // OK (subclass)
}
Note
Changing only the unchecked exceptions never breaks the override contract.
Important
Remember: constructors follow a different rule.
A Constructor must declare all the checked exceptions declared in the base constructor (or the superclasses of those checked exceptions).
It may also declare additional checked exceptions. This behavior is the opposite of method overriding.
An overriding method cannot throw any checked exception other than those declared by the overridden method. It may only throw subclasses of those exceptions.
19.4 Handling exceptions: try, catch, finally
19.4.1 Basic try-catch syntax
try {
riskyOperation();
} catch (IOException e) {
handle(e);
}
- A
tryblock must be followed by at least onecatchor afinally - Catches are checked top-down
19.4.2 Multiple catch blocks
Multiple catch blocks allow different handling for different exception types.
try {
process();
} catch (FileNotFoundException e) {
recover();
} catch (IOException e) {
log();
}
Note
More specific exceptions must come before more general ones, otherwise compilation fails.
If you place a catch for a superclass (e.g. IOException) before a catch for a subclass (e.g. FileNotFoundException), the subclass catch becomes unreachable.
19.4.3 Multi-catch (Java 7+)
try {
process();
} catch (IOException | SQLException e) {
log(e);
}
- Exception types must be unrelated (no parent/child)
- The caught variable is implicitly
final
19.4.4 finally block
The finally block executes regardless of whether an exception is thrown, except in extreme JVM termination cases.
try {
open();
} finally {
close();
}
- Used for cleanup logic
- Executes even if
returnis used in try and/or catch block
Note
A finally block can override a return value or swallow an exception. This is generally discouraged because it makes the control flow harder to reason about.
Important
When both a catch block and a finally block throw exceptions, the exception thrown in the finally block is the one that is propagated from the method.
The exception thrown in the catch block is lost and is not added to the suppressed exceptions list.
try {
throw new RuntimeException("try");
} catch (RuntimeException e) {
throw new RuntimeException("catch");
} finally {
throw new RuntimeException("finally");
}
In this case, only the "finally" exception is thrown.
19.5 Automatic Resource Management (try-with-resources)
Try-with-resources provides automatic closing of resources that implement AutoCloseable.
It eliminates the need for explicit finally cleanup in most cases.
19.5.1 Basic syntax
try (BufferedReader br = Files.newBufferedReader(path)) {
return br.readLine();
}
- Resources are closed automatically
- Closure happens even if an exception is thrown
- Resources are closed before any catch or finally block executes.
try (Resource a = new Resource()) {
a.read();
} finally {
a.close(); // ❌ Compile-time error: a is out of scope here
}
19.5.2 Declaring multiple resources
try (InputStream in = Files.newInputStream(p);
OutputStream out = Files.newOutputStream(q)) {
in.transferTo(out);
}
- Resources are closed in reverse order of declaration
19.5.3 Scope of resources
- Resources are in scope only inside the
tryblock - They are implicitly
final - Since Java 9, you can declare resources ahead of time, outside the
try-with-resources, provided they are declared asfinalor are effectively final.
final var firstWriter = Files.newBufferedWriter(filePath);
try (firstWriter; var secondWriter = Files.newBufferedWriter(filePath)) {
// CODE
}
Note
Attempting to reassign a resource variable causes a compilation error.
Resource a = new Resource();
try(a){ // since Java 9
...
}finally{
a.close(); // this code will compile but the resource referred to by the reference 'a', has been closed.
}
19.6 Suppressed exceptions
When both the try block and the resource’s close() method throw exceptions, Java preserves the primary exception and suppresses the others.
try (BadResource r = new BadResource()) {
throw new RuntimeException("main");
}
If close() also throws an exception, it becomes suppressed.
catch (Exception e) {
for (Throwable t : e.getSuppressed()) {
System.out.println(t);
}
}
- Primary exception is thrown
- Secondary exceptions are accessible via
getSuppressed()
Important
Suppressed exceptions are generated only by the implicit finally block created by try-with-resources.
In contrast, exceptions thrown in an explicit finally block are not suppressed: they replace any previous exception and become the only exception propagated.
19.7 Exceptions summary
- Checked exceptions must be caught or declared
- Overriding methods may not widen checked exceptions
- Use multi-catch for shared handling logic
- Prefer try-with-resources over finally cleanup
- Resources close in reverse order
- Suppressed exceptions preserve full failure context