Java / Intermediate / Lesson 9
Exceptions, resource ownership, and absence
Design failure paths that preserve meaning and cleanup. Distinguish checked failures, programming defects, missing results, and resource lifecycle.
On this page
Lesson goals
- Catch failures where a useful policy exists.
- Use try-with-resources and preserve causes.
- Represent optional results without hiding errors.
Separate invalid input from operational failure
An exception interrupts normal control flow and carries a type, message, stack trace, and potentially a cause. Checked exceptions require handling or declaration; unchecked exceptions include many contract violations and programming defects. This distinction does not mean checked failures are always recoverable or unchecked failures can be ignored. Decide policy from the operation and boundary rather than the inheritance label alone.
A malformed quantity, an unavailable file, and an internal invariant violation deserve different responses. Reject invalid user data with useful feedback, translate expected operational failure at a suitable boundary, and let unexpected defects remain observable. Catching Exception everywhere and returning a normal value hides the difference and can cause later code to act on fabricated success.
Catch narrowly and retain diagnostic meaning
The parsing example catches only NumberFormatException and reports an explicit rejected result. It does not silently turn 'twelve' into zero. The input is synthetic and the output is safe to display. Real applications should avoid logging sensitive raw input while still recording an operation identifier and enough restricted context to diagnose the boundary.
A catch block should recover, translate with a preserved cause, or propagate; merely printing a stack trace and continuing is usually an incomplete policy. If wrapping a failure, keep the original exception as the cause rather than copying only its message. Do not reveal internal paths, secrets, or stack traces to ordinary users when a stable public error response is sufficient.
public class Main {
public static void main(String[] args) throws Exception {
try {
int quantity=Integer.parseInt("twelve");
System.out.println(quantity);
} catch(NumberFormatException ex) {
System.out.println("invalid quantity");
}
}
}
invalid quantity
Parsing fails before quantity is used, and only the explicit rejection branch prints.
Give cleanup an owner
A resource such as a reader, stream, or database connection often requires prompt close independently of garbage collection. try-with-resources closes AutoCloseable resources when control leaves the block, including exceptional paths, in reverse declaration order. Use the narrowest lifetime that contains the required work. Do not close a shared resource you do not own merely because its type supports close.
The example uses a StringReader so no real filesystem is touched. It reads one line and closes the BufferedReader on exit. If both the body and close fail, try-with-resources preserves the primary failure and records suppressed cleanup exceptions. A manual finally block that throws a new exception can accidentally obscure the original cause, so inspect suppressed failures when diagnosing resource problems.
public class Main {
public static void main(String[] args) throws Exception {
try(var reader=new java.io.BufferedReader(new java.io.StringReader("alpha\nbeta"))) {
System.out.println(reader.readLine());
}
}
}
alpha
The first line is alpha. The reader is closed when the block ends, without relying on garbage collection.
Use absence as a result, not a disguised error
Optional<T> can represent a method result that legitimately may be absent. It is not a universal replacement for every nullable field or every failure. A search with no match may return Optional.empty, while a failed data source should remain a failure unless the contract explicitly equates it with absence. Do not call get without proving presence.
map and flatMap can transform a present value, while orElseGet evaluates a fallback lazily. An eager fallback argument can perform unnecessary work even when a value exists. Do not return null where Optional is promised, and avoid nesting containers unnecessarily. A clear API explains whether an empty result means not found, not applicable, or another precise state.
Test failure and cleanup as first-class behavior
A useful test injects failure after a resource is acquired and verifies cleanup, not only the normal return. Test parsing boundaries, missing values, close failures when relevant, and exception translation with the original cause preserved. Resource ownership should be visible in the API and control structure rather than a comment hoping another layer will close it eventually.
Do not use exceptions for ordinary loop termination when a normal result such as end-of-input exists. Conversely, do not convert an exceptional failed operation into an empty collection just to simplify a caller. The right boundary keeps valid absence, rejected input, operational failure, and programming defects distinguishable so the application can make a correct decision.
Common errors and fixes
NoSuchElementException from Optional.get
- Why it happens
- Code assumed an optional value was present.
- How to fix it
- Use a defined empty-result branch or transform with Optional operations.
- Prevent it next time
- Test absent and present outcomes.
A file or reader remains open after failure
- Why it happens
- Cleanup was placed only on the success path.
- How to fix it
- Use try-with-resources for owned AutoCloseable resources.
- Prevent it next time
- Inject failure after acquisition in tests.
The original exception disappeared
- Why it happens
- A wrapper omitted the cause or cleanup replaced the primary failure.
- How to fix it
- Preserve the cause and inspect suppressed exceptions.
- Prevent it next time
- Review exceptional paths and public error translation.
Check your understanding
1. A search backend is unavailable. Should its method return Optional.empty?
Show worked solution
Only if the documented domain intentionally equates outage with no result, which is usually misleading. Preserve the operational failure so the caller can retry or report unavailability; reserve empty for a successful search with no match.
2. The body throws while closing also throws. Which evidence should be retained?
Show worked solution
The body failure should remain primary, with the cleanup failure suppressed. try-with-resources implements this relationship. Inspect getSuppressed during diagnosis and keep the causal chain in restricted logs rather than exposing internal details publicly.
Key takeaways
- Failure policy belongs at a meaningful boundary.
- Prompt resource cleanup is independent of garbage collection.
- Absence and failed retrieval are different outcomes.