Java foundations · Lesson 37 About 85 min with practice
Java Multiple Exceptions and Try-with-Resources
Handle different failures deliberately, then let Java manage the lifetime of resources you own. Learn catch ordering, multi-catch, reverse closing, and suppressed cleanup failures through predictable runnable examples.
Choose separate catches when failures need different handling.
Use multi-catch only for compatible alternatives with a shared response.
Explain who owns and closes an AutoCloseable resource.
Trace reverse closing order and cleanup after failed initialization.
Distinguish a primary failure, its cause, and its suppressed cleanup failures.
Publish a successful result only after reading, parsing, validation, and closing succeed.
Different failures can need different next actions
Reading a minutes field has several stages: acquire a source, read text, parse a number, validate the range, and close the source. Invalid numeric text is not the same as a failure to obtain that text. A caller should not report every failure as please enter a number.
The first example uses two catch clauses. NumberFormatException gives an input-specific response, while IOException describes the simulated read failure. Only the first compatible handler is selected for a thrown exception; the handlers do not all run in sequence after every attempt.
DemoSource is an in-memory teaching resource with deliberate failure switches and an event log. It does not open a real file, connect to a server, or save learning progress. IOException is used to model resource failures reproducibly before the following lessons introduce actual file operations.
JAVA 21 COMPILER
Main.java · Give parsing and I/O different responses
Predict the result, edit Main.java, then run your program.
importjava.io.IOException;publicclassMain{publicstaticvoidmain(String[] args){for(boolean failRead :newboolean[]{false,true}){StringBuilder events =newStringBuilder();try{System.out.println("Accepted: "+MinutesLoader.load("oops", failRead,false, events));}catch(NumberFormatException error){System.out.println("Input error: enter a whole number.");}catch(IOException error){System.out.println("I/O error: "+ error.getMessage());}System.out.print(events);}}}classDemoSourceimplementsAutoCloseable{privatefinalString text;privatefinalboolean failRead;privatefinalboolean failClose;privatefinalStringBuilder events;privateboolean closed;publicDemoSource(String text,boolean failRead,boolean failClose,StringBuilder events){if(text ==null|| events ==null){thrownewIllegalArgumentException("Text and events must not be null.");}this.text = text;this.failRead = failRead;this.failClose = failClose;this.events = events;
events.append("Opened\n");}publicStringread()throwsIOException{if(closed)thrownewIOException("Source is closed.");if(failRead)thrownewIOException("Read failed.");return text;}@Overridepublicvoidclose()throwsIOException{if(closed)return;
closed =true;
events.append("Closed\n");if(failClose)thrownewIOException("Close failed.");}}classMinutesLoader{publicstaticintload(String text,boolean failRead,boolean failClose,StringBuilder events)throwsIOException{try(DemoSource source =newDemoSource(text, failRead, failClose, events)){int minutes =Integer.parseInt(source.read().strip());if(minutes <1|| minutes >180){thrownewIllegalArgumentException("Minutes must be between 1 and 180.");}return minutes;}}}
Standard input · optional
TERMINAL OUTPUT
Select Run code to compile and execute this program.
Expected output · predict acquisition, body, closing, and handling separately
Input error: enter a whole number.
Opened
Closed
I/O error: Read failed.
Opened
Closed
Put more specific handlers before their broader parents
A catch for a superclass also matches its compatible subclasses. NumberFormatException extends IllegalArgumentException, so a catch for NumberFormatException must precede a later catch for IllegalArgumentException when both are used. Reversing them makes the specific handler unreachable and fails compilation.
Catch order is not a scoring system that chooses the most specific handler anywhere in the list. The first compatible handler wins. If NumberFormatException and IOException are used as in the first example, neither is the other's superclass, so their relative order does not change which one matches.
Design handlers around what the boundary can meaningfully do. Keep unexpected programming defects and serious Error conditions from being disguised as ordinary input rejection. A handler that logs or displays an error is not automatically a recovery strategy or permission to continue a damaged workflow.
Use multi-catch when the response genuinely is the same
The vertical bar joins alternative types in one catch parameter. NumberFormatException | IOException is useful here because this short example only reports that the attempt was rejected, regardless of whether numeric conversion or the resource operation failed.
The alternatives cannot have a subclass relationship. NumberFormatException | IllegalArgumentException is invalid because one alternative already covers the other. A multi-catch parameter is also implicitly final, so the handler cannot reassign error to a different exception object.
Do not combine handlers merely to make the source shorter. If a user should correct text for one failure but a developer needs to investigate source availability for another, separate handlers express those different responses more clearly. Multi-catch shares handling code; it does not merge several simultaneous thrown exceptions into one.
JAVA 21 COMPILER
Main.java · Share one handler for unrelated exception types
Predict the result, edit Main.java, then run your program.
importjava.io.IOException;publicclassMain{publicstaticvoidmain(String[] args){StringBuilder events =newStringBuilder();try{MinutesLoader.load("oops",false,false, events);}catch(NumberFormatException|IOException error){System.out.println("Attempt rejected: "+ error.getClass().getSimpleName());}System.out.print(events);}}classDemoSourceimplementsAutoCloseable{privatefinalString text;privatefinalboolean failRead;privatefinalboolean failClose;privatefinalStringBuilder events;privateboolean closed;publicDemoSource(String text,boolean failRead,boolean failClose,StringBuilder events){if(text ==null|| events ==null){thrownewIllegalArgumentException("Text and events must not be null.");}this.text = text;this.failRead = failRead;this.failClose = failClose;this.events = events;
events.append("Opened\n");}publicStringread()throwsIOException{if(closed)thrownewIOException("Source is closed.");if(failRead)thrownewIOException("Read failed.");return text;}@Overridepublicvoidclose()throwsIOException{if(closed)return;
closed =true;
events.append("Closed\n");if(failClose)thrownewIOException("Close failed.");}}classMinutesLoader{publicstaticintload(String text,boolean failRead,boolean failClose,StringBuilder events)throwsIOException{try(DemoSource source =newDemoSource(text, failRead, failClose, events)){int minutes =Integer.parseInt(source.read().strip());if(minutes <1|| minutes >180){thrownewIllegalArgumentException("Minutes must be between 1 and 180.");}return minutes;}}}
Standard input · optional
TERMINAL OUTPUT
Select Run code to compile and execute this program.
Expected output · predict acquisition, body, closing, and handling separately
Attempt rejected: NumberFormatException
Opened
Closed
Try-with-resources expresses who is responsible for closing
A resource declaration appears in parentheses after try. Its type must implement AutoCloseable or a suitable subtype. Java arranges to invoke close as the resource scope finishes, including when the body returns or throws, without requiring a manually written finally for that closing operation.
The source declared in the header belongs to that try's resource scope. Work that needs it happens inside the body, while catch and finally attached to this statement run after automatic closing has been attempted. The source name declared there is not available as an ordinary local after the statement.
Do not put a borrowed caller-owned resource into a closing scope without an ownership agreement. Closing a wrapper often closes its underlying stream too. Nor does every AutoCloseable promise that repeated close calls are harmless: DemoSource deliberately makes close idempotent, but that is this class's contract rather than a universal interface guarantee.
JAVA 21 COMPILER
Main.java · Read inside the owned resource scope
Predict the result, edit Main.java, then run your program.
importjava.io.IOException;publicclassMain{publicstaticvoidmain(String[] args){StringBuilder events =newStringBuilder();try(DemoSource source =newDemoSource("45",false,false, events)){System.out.println("Read: "+ source.read());}catch(IOException error){System.out.println("I/O error: "+ error.getMessage());}System.out.print(events);}}classDemoSourceimplementsAutoCloseable{privatefinalString text;privatefinalboolean failRead;privatefinalboolean failClose;privatefinalStringBuilder events;privateboolean closed;publicDemoSource(String text,boolean failRead,boolean failClose,StringBuilder events){if(text ==null|| events ==null){thrownewIllegalArgumentException("Text and events must not be null.");}this.text = text;this.failRead = failRead;this.failClose = failClose;this.events = events;
events.append("Opened\n");}publicStringread()throwsIOException{if(closed)thrownewIOException("Source is closed.");if(failRead)thrownewIOException("Read failed.");return text;}@Overridepublicvoidclose()throwsIOException{if(closed)return;
closed =true;
events.append("Closed\n");if(failClose)thrownewIOException("Close failed.");}}classMinutesLoader{publicstaticintload(String text,boolean failRead,boolean failClose,StringBuilder events)throwsIOException{try(DemoSource source =newDemoSource(text, failRead, failClose, events)){int minutes =Integer.parseInt(source.read().strip());if(minutes <1|| minutes >180){thrownewIllegalArgumentException("Minutes must be between 1 and 180.");}return minutes;}}}
Standard input · optional
TERMINAL OUTPUT
Select Run code to compile and execute this program.
Expected output · predict acquisition, body, closing, and handling separately
Read: 45
Opened
Closed
Acquire left to right and close in the opposite order
The two resource declarations are initialized from left to right, so A opens before B. When the body finishes, B closes before A. Reverse order is useful when the later resource depends on the earlier one, such as a buffered wrapper depending on an underlying stream.
The output makes the lifetime visible without touching a real file. Body appears only after both initializations succeed. Both close calls occur before a surrounding handler would report a failure from the resource statement.
Keep each resource's responsibility small and be deliberate about wrappers and duplicate ownership. Reverse close order does not justify closing the same underlying resource from unrelated owners, and a real resource may behave differently after its first close than the tiny demonstration object.
JAVA 21 COMPILER
Main.java · Close successful acquisitions in reverse order
Predict the result, edit Main.java, then run your program.
importjava.io.IOException;publicclassMain{publicstaticvoidmain(String[] args){try(NamedResource first =newNamedResource("A",false);NamedResource second =newNamedResource("B",false)){System.out.println("Body");}catch(IOException error){System.out.println(error.getMessage());}}}classNamedResourceimplementsAutoCloseable{privatefinalString name;publicNamedResource(String name,boolean failOpen)throwsIOException{this.name = name;System.out.println("Opening "+ name);if(failOpen)thrownewIOException("Open failed: "+ name);}@Overridepublicvoidclose(){System.out.println("Closing "+ name);}}
Standard input · optional
TERMINAL OUTPUT
Select Run code to compile and execute this program.
Expected output · predict acquisition, body, closing, and handling separately
Opening A
Opening B
Body
Closing B
Closing A
Trace the two-resource scope
Construct A successfully.
Construct B successfully.
Run the body.
Close B, the later acquisition.
Close A, then leave the resource statement.
If closing B fails, closing A is still attempted. A failure does not simply abandon the rest of the cleanup sequence.
A later acquisition failure still closes earlier acquired resources
In this example B's constructor prints Opening B and then throws. The try body is never entered. A had already been acquired successfully, so Java closes A before the catch reports B's acquisition failure.
There is no automatic close call on B because its initialization never completed and no resource value was acquired for that declaration. A constructor that partially acquires its own external handles before failing must clean up its internal partial work; the caller's header cannot close an object it never received.
This distinction is why acquisition deserves tests, not just body and close paths. If the first resource fails to initialize, there are no earlier resources from that header to close. If a later acquisition fails and an earlier close also fails, the acquisition failure remains primary and the closing failure is normally suppressed.
JAVA 21 COMPILER
Main.java · Clean up earlier resources when a later acquisition fails
Predict the result, edit Main.java, then run your program.
importjava.io.IOException;publicclassMain{publicstaticvoidmain(String[] args){try(NamedResource first =newNamedResource("A",false);NamedResource second =newNamedResource("B",true)){System.out.println("Body");}catch(IOException error){System.out.println(error.getMessage());}}}classNamedResourceimplementsAutoCloseable{privatefinalString name;publicNamedResource(String name,boolean failOpen)throwsIOException{this.name = name;System.out.println("Opening "+ name);if(failOpen)thrownewIOException("Open failed: "+ name);}@Overridepublicvoidclose(){System.out.println("Closing "+ name);}}
Standard input · optional
TERMINAL OUTPUT
Select Run code to compile and execute this program.
Expected output · predict acquisition, body, closing, and handling separately
Opening A
Opening B
Closing A
Open failed: B
Preserve the primary operation failure and the cleanup evidence
The read-and-close example deliberately fails twice. The body first throws IOException with Read failed. Automatic close is still attempted and throws another IOException with Close failed. The read failure remains the primary thrown exception.
The close failure is attached to that primary exception's suppressed list. getSuppressed returns the retained cleanup failures so they can be inspected without replacing the original diagnosis. With multiple failing resources, closing continues in reverse order; later close failures are retained alongside the primary one.
A suppressed exception is not the same as a cause. A cause explains a translated or underlying failure; suppression records another failure encountered while an earlier outcome was already in progress. In our example the primary exception has no cause, but it does have one suppressed close failure.
JAVA 21 COMPILER
Main.java · Retain the read failure and inspect the close failure
Predict the result, edit Main.java, then run your program.
importjava.io.IOException;publicclassMain{publicstaticvoidmain(String[] args){StringBuilder events =newStringBuilder();try{MinutesLoader.load("45",true,true, events);}catch(IOException error){System.out.println("Primary: "+ error.getMessage());for(Throwable cleanup : error.getSuppressed()){System.out.println("Suppressed: "+ cleanup.getMessage());}System.out.println("Cause absent: "+(error.getCause()==null));}System.out.print(events);}}classDemoSourceimplementsAutoCloseable{privatefinalString text;privatefinalboolean failRead;privatefinalboolean failClose;privatefinalStringBuilder events;privateboolean closed;publicDemoSource(String text,boolean failRead,boolean failClose,StringBuilder events){if(text ==null|| events ==null){thrownewIllegalArgumentException("Text and events must not be null.");}this.text = text;this.failRead = failRead;this.failClose = failClose;this.events = events;
events.append("Opened\n");}publicStringread()throwsIOException{if(closed)thrownewIOException("Source is closed.");if(failRead)thrownewIOException("Read failed.");return text;}@Overridepublicvoidclose()throwsIOException{if(closed)return;
closed =true;
events.append("Closed\n");if(failClose)thrownewIOException("Close failed.");}}classMinutesLoader{publicstaticintload(String text,boolean failRead,boolean failClose,StringBuilder events)throwsIOException{try(DemoSource source =newDemoSource(text, failRead, failClose, events)){int minutes =Integer.parseInt(source.read().strip());if(minutes <1|| minutes >180){thrownewIllegalArgumentException("Minutes must be between 1 and 180.");}return minutes;}}}
Standard input · optional
TERMINAL OUTPUT
Select Run code to compile and execute this program.
Expected output · predict acquisition, body, closing, and handling separately
Primary: Read failed.
Suppressed: Close failed.
Cause absent: true
Opened
Closed
Read the two-failure case in order
The source opens successfully.
read throws Read failed.
Automatic close is attempted and throws Close failed.
The read failure remains primary; the close failure is attached to getSuppressed().
The caller handles the primary exception and inspects retained cleanup evidence.
getCause() and getSuppressed() answer different diagnostic questions. Do not assume one substitutes for the other.
Successful computation does not guarantee successful resource completion
MinutesLoader calculates a valid number and reaches return inside its resource scope. Closing happens before the caller receives that value. When close alone fails, the IOException from close is the primary failure and the caller's Accepted line is not reached.
This differs from a body failure with a suppressed close failure: here no body exception existed to preserve. The close exception therefore has an empty suppressed list in this one-resource example. A pending return is not an earlier exception to which a cleanup failure can be attached.
Put success reporting after the operation returns successfully rather than announcing acceptance inside its try body before resources finish. That still does not provide automatic rollback for work already done. Closing failure and transactional recovery require their own domain policies in real systems.
JAVA 21 COMPILER
Main.java · A close failure prevents a pending successful return
Predict the result, edit Main.java, then run your program.
importjava.io.IOException;publicclassMain{publicstaticvoidmain(String[] args){StringBuilder events =newStringBuilder();try{int minutes =MinutesLoader.load("45",false,true, events);System.out.println("Accepted: "+ minutes);}catch(IOException error){System.out.println("Primary: "+ error.getMessage());System.out.println("Suppressed count: "+ error.getSuppressed().length);}System.out.print(events);}}classDemoSourceimplementsAutoCloseable{privatefinalString text;privatefinalboolean failRead;privatefinalboolean failClose;privatefinalStringBuilder events;privateboolean closed;publicDemoSource(String text,boolean failRead,boolean failClose,StringBuilder events){if(text ==null|| events ==null){thrownewIllegalArgumentException("Text and events must not be null.");}this.text = text;this.failRead = failRead;this.failClose = failClose;this.events = events;
events.append("Opened\n");}publicStringread()throwsIOException{if(closed)thrownewIOException("Source is closed.");if(failRead)thrownewIOException("Read failed.");return text;}@Overridepublicvoidclose()throwsIOException{if(closed)return;
closed =true;
events.append("Closed\n");if(failClose)thrownewIOException("Close failed.");}}classMinutesLoader{publicstaticintload(String text,boolean failRead,boolean failClose,StringBuilder events)throwsIOException{try(DemoSource source =newDemoSource(text, failRead, failClose, events)){int minutes =Integer.parseInt(source.read().strip());if(minutes <1|| minutes >180){thrownewIllegalArgumentException("Minutes must be between 1 and 180.");}return minutes;}}}
Standard input · optional
TERMINAL OUTPUT
Select Run code to compile and execute this program.
Expected output · predict acquisition, body, closing, and handling separately
Primary: Close failed.
Suppressed count: 0
Opened
Closed
Scope of a reference is different from lifetime of the resource
Java 21 also permits an existing final or effectively final local reference in the resource header. The example initializes source once and uses try (source). Because the local is declared outside, its name is still visible afterward, but the resource has already been closed.
Our DemoSource rejects a subsequent read with Source is closed. A reference remaining in scope does not keep the resource open or reopen it automatically. Reassigning the captured resource variable would also violate the final-or-effectively-final requirement for this header form.
Resources declared directly in a header cannot be reassigned either. A null resource reference is skipped during automatic closing, but using it inside the body would still be an ordinary null dereference. Resource syntax manages closing; it does not replace input validation or make every object non-null.
JAVA 21 COMPILER
Main.java · An effectively final resource reference still has a closed lifetime
Predict the result, edit Main.java, then run your program.
importjava.io.IOException;publicclassMain{publicstaticvoidmain(String[] args){StringBuilder events =newStringBuilder();DemoSource source =newDemoSource("60",false,false, events);try(source){System.out.println("Read: "+ source.read());}catch(IOException error){System.out.println(error.getMessage());}try{
source.read();}catch(IOException error){System.out.println("After scope: "+ error.getMessage());}System.out.print(events);}}classDemoSourceimplementsAutoCloseable{privatefinalString text;privatefinalboolean failRead;privatefinalboolean failClose;privatefinalStringBuilder events;privateboolean closed;publicDemoSource(String text,boolean failRead,boolean failClose,StringBuilder events){if(text ==null|| events ==null){thrownewIllegalArgumentException("Text and events must not be null.");}this.text = text;this.failRead = failRead;this.failClose = failClose;this.events = events;
events.append("Opened\n");}publicStringread()throwsIOException{if(closed)thrownewIOException("Source is closed.");if(failRead)thrownewIOException("Read failed.");return text;}@Overridepublicvoidclose()throwsIOException{if(closed)return;
closed =true;
events.append("Closed\n");if(failClose)thrownewIOException("Close failed.");}}classMinutesLoader{publicstaticintload(String text,boolean failRead,boolean failClose,StringBuilder events)throwsIOException{try(DemoSource source =newDemoSource(text, failRead, failClose, events)){int minutes =Integer.parseInt(source.read().strip());if(minutes <1|| minutes >180){thrownewIllegalArgumentException("Minutes must be between 1 and 180.");}return minutes;}}}
Standard input · optional
TERMINAL OUTPUT
Select Run code to compile and execute this program.
Expected output · predict acquisition, body, closing, and handling separately
Read: 60
After scope: Source is closed.
Opened
Closed
Verify acquisition, work, and close as separate boundaries
A useful test matrix covers normal completion, malformed input, domain-range rejection, read failure, close-only failure, and simultaneous body/close failure. Verify both the selected handler and the resource events, not just the last message displayed to a user.
The independent lab keeps parsing and I/O responses distinct and prints suppressed cleanup evidence where it exists. Its event buffer is displayed after the result handler, so Opened and Closed appear later on screen even though those events happened during the loader call. This is buffered observation, not evidence that cleanup happened after the catch.
Also test reverse close order, earlier-resource cleanup after failed initialization, and compiler errors for bad catch order or illegal multi-catch alternatives. These checks preserve the language rules while ensuring the final caller does not swallow unrelated defects or report a result before closing succeeds.
Repair exercise: a specific handler is unreachable
The broad IllegalArgumentException handler appears before its NumberFormatException subtype, making the later handler unreachable. Repair the order while preserving separate numeric, range, and I/O responses. The starter intentionally fails compilation.
JAVA 21 COMPILER
Main.java · Diagnose unreachable catch order
Predict the result, edit Main.java, then run your program.
importjava.io.IOException;publicclassMain{publicstaticvoidmain(String[] args){StringBuilder events =newStringBuilder();try{MinutesLoader.load("oops",false,false, events);}catch(IllegalArgumentException error){System.out.println("Range rejected");}catch(NumberFormatException error){System.out.println("Numeric text rejected");}catch(IOException error){System.out.println("I/O rejected");}System.out.print(events);}}classDemoSourceimplementsAutoCloseable{privatefinalString text;privatefinalboolean failRead;privatefinalboolean failClose;privatefinalStringBuilder events;privateboolean closed;publicDemoSource(String text,boolean failRead,boolean failClose,StringBuilder events){if(text ==null|| events ==null){thrownewIllegalArgumentException("Text and events must not be null.");}this.text = text;this.failRead = failRead;this.failClose = failClose;this.events = events;
events.append("Opened\n");}publicStringread()throwsIOException{if(closed)thrownewIOException("Source is closed.");if(failRead)thrownewIOException("Read failed.");return text;}@Overridepublicvoidclose()throwsIOException{if(closed)return;
closed =true;
events.append("Closed\n");if(failClose)thrownewIOException("Close failed.");}}classMinutesLoader{publicstaticintload(String text,boolean failRead,boolean failClose,StringBuilder events)throwsIOException{try(DemoSource source =newDemoSource(text, failRead, failClose, events)){int minutes =Integer.parseInt(source.read().strip());if(minutes <1|| minutes >180){thrownewIllegalArgumentException("Minutes must be between 1 and 180.");}return minutes;}}}
Standard input · optional
TERMINAL OUTPUT
Select Run code to compile and execute this program.
The starter intentionally fails compilation. Look for a diagnostic that NumberFormatException has already been caught. No runtime output should execute until the handler order is repaired.
Reveal the explained repair
The first compatible catch is selected, so specific subclass handlers must come before compatible superclass handlers. IOException is unrelated to this numeric hierarchy. The repair restores distinct responses instead of deleting the type-specific behavior.
JAVA 21 COMPILER
Main.java · Handle specific failures first
Predict the result, edit Main.java, then run your program.
importjava.io.IOException;publicclassMain{publicstaticvoidmain(String[] args){StringBuilder events =newStringBuilder();try{MinutesLoader.load("oops",false,false, events);}catch(NumberFormatException error){System.out.println("Numeric text rejected");}catch(IllegalArgumentException error){System.out.println("Range rejected");}catch(IOException error){System.out.println("I/O rejected");}System.out.print(events);}}classDemoSourceimplementsAutoCloseable{privatefinalString text;privatefinalboolean failRead;privatefinalboolean failClose;privatefinalStringBuilder events;privateboolean closed;publicDemoSource(String text,boolean failRead,boolean failClose,StringBuilder events){if(text ==null|| events ==null){thrownewIllegalArgumentException("Text and events must not be null.");}this.text = text;this.failRead = failRead;this.failClose = failClose;this.events = events;
events.append("Opened\n");}publicStringread()throwsIOException{if(closed)thrownewIOException("Source is closed.");if(failRead)thrownewIOException("Read failed.");return text;}@Overridepublicvoidclose()throwsIOException{if(closed)return;
closed =true;
events.append("Closed\n");if(failClose)thrownewIOException("Close failed.");}}classMinutesLoader{publicstaticintload(String text,boolean failRead,boolean failClose,StringBuilder events)throwsIOException{try(DemoSource source =newDemoSource(text, failRead, failClose, events)){int minutes =Integer.parseInt(source.read().strip());if(minutes <1|| minutes >180){thrownewIllegalArgumentException("Minutes must be between 1 and 180.");}return minutes;}}}
Standard input · optional
TERMINAL OUTPUT
Select Run code to compile and execute this program.
Numeric text rejected
Opened
Closed
Independent lab: read, parse, validate, and close before acceptance
Read one synthetic minutes value from an owned AutoCloseable source, distinguish numeric/range/I/O failures, and retain cleanup diagnostics. Use in-memory event recording and failure switches; do not create files, access a server, or save progress.
Implement DemoSource with text, failRead, failClose, and events constructor arguments. Reject null text or events with IllegalArgumentException and Text and events must not be null. Record Opened followed by a newline after successful initialization.
Implement read() throws IOException: reject a closed source with Source is closed., simulate configured read failure with Read failed., or return the text. Implement close() throws IOException: on the first call set closed, record Closed and a newline, then simulate Close failed. when configured; later close calls do nothing.
Implement static MinutesLoader.load(String, boolean, boolean, StringBuilder) throws IOException. Create and own DemoSource in a try-with-resources header, strip and parse the returned text, and reject values outside 1..180 with IllegalArgumentException and Minutes must be between 1 and 180. Return the parsed value inside that scope.
Keep the source and loader free of console output. Buffer resource events for the caller to display. Do not catch failures inside the loader merely to substitute zero; allow parsing, range, and I/O failures to reach the boundary.
Use the supplied five cases in main. Catch NumberFormatException before IllegalArgumentException, and handle IOException separately. Only print Accepted after load returns. Print suppressed cleanup messages for rejected attempts, then display the recorded events.
Verify normal completion, numeric rejection, range rejection, read failure, close-only failure, and simultaneous body/close failure. Also test reverse close order and initialization failure separately. Keep causes and suppressed failures conceptually distinct.
The final malformed-input attempt also has a closing failure. Keep the numeric failure primary and show the cleanup evidence. The completed program should produce:
Attempt 1
Accepted: 45
Opened
Closed
Attempt 2
Input error: enter a whole number.
Opened
Closed
Attempt 3
I/O error: Read failed.
Opened
Closed
Attempt 4
I/O error: Close failed.
Opened
Closed
Attempt 5
Input error: enter a whole number.
Suppressed: Close failed.
Opened
Closed
JAVA 21 COMPILER
Main.java · Build the owned-source loader
Predict the result, edit Main.java, then run your program.
importjava.io.IOException;publicclassMain{publicstaticvoidmain(String[] args){// TODO: define the five inputs and failure switches.// TODO: load each value and handle numeric, range, and I/O failures.// TODO: inspect suppressed failures and display buffered events.}}classDemoSourceimplementsAutoCloseable{// TODO: state, constructor, read(), and idempotent close().@Overridepublicvoidclose()throwsIOException{}}classMinutesLoader{// TODO: owned resource scope, parsing, validation, and return.}
Standard input · optional
TERMINAL OUTPUT
Select Run code to compile and execute this program.
Stage 1 hints: define the resource contract
Store the supplied configuration and event buffer. read checks closed state before the simulated read failure. On its first close, mark closed and append the event before optionally throwing. Do not print from the resource; do not claim every AutoCloseable has these repeat-close rules.
Stage 2 hints: own the scope and preserve failures
Construct DemoSource directly in the loader's try-with-resources header. Read, strip, parse, and range-check inside the body, then return. Let Java retain suppression rather than manually catching cleanup and replacing the original failure.
Stage 3 hints: distinguish caller responses
Catch the numeric subtype before the broader range exception, and handle IOException separately. Only announce acceptance after the loader returns. Inspect each primary failure's suppressed list, then display the buffered events.
Reveal the complete explained solution
JAVA 21 COMPILER
Main.java · Complete owned-source loader lab
Predict the result, edit Main.java, then run your program.
importjava.io.IOException;publicclassMain{publicstaticvoidmain(String[] args){String[] text ={"45","oops","60","90","oops"};boolean[] readFailure ={false,false,true,false,false};boolean[] closeFailure ={false,false,false,true,true};for(int index =0; index < text.length; index++){StringBuilder events =newStringBuilder();System.out.println("Attempt "+(index +1));try{int minutes =MinutesLoader.load(text[index], readFailure[index], closeFailure[index], events);System.out.println("Accepted: "+ minutes);}catch(NumberFormatException error){System.out.println("Input error: enter a whole number.");showSuppressed(error);}catch(IllegalArgumentException error){System.out.println("Range error: "+ error.getMessage());showSuppressed(error);}catch(IOException error){System.out.println("I/O error: "+ error.getMessage());showSuppressed(error);}System.out.print(events);}}staticvoidshowSuppressed(Throwable error){for(Throwable cleanup : error.getSuppressed()){System.out.println("Suppressed: "+ cleanup.getMessage());}}}classDemoSourceimplementsAutoCloseable{privatefinalString text;privatefinalboolean failRead;privatefinalboolean failClose;privatefinalStringBuilder events;privateboolean closed;publicDemoSource(String text,boolean failRead,boolean failClose,StringBuilder events){if(text ==null|| events ==null){thrownewIllegalArgumentException("Text and events must not be null.");}this.text = text;this.failRead = failRead;this.failClose = failClose;this.events = events;
events.append("Opened\n");}publicStringread()throwsIOException{if(closed)thrownewIOException("Source is closed.");if(failRead)thrownewIOException("Read failed.");return text;}@Overridepublicvoidclose()throwsIOException{if(closed)return;
closed =true;
events.append("Closed\n");if(failClose)thrownewIOException("Close failed.");}}classMinutesLoader{publicstaticintload(String text,boolean failRead,boolean failClose,StringBuilder events)throwsIOException{try(DemoSource source =newDemoSource(text, failRead, failClose, events)){int minutes =Integer.parseInt(source.read().strip());if(minutes <1|| minutes >180){thrownewIllegalArgumentException("Minutes must be between 1 and 180.");}return minutes;}}}
Standard input · optional
TERMINAL OUTPUT
Select Run code to compile and execute this program.
The source is closed on every successful acquisition, even when parsing or reading fails. Numeric rejection stays primary when its close also fails, while a close-only IOException prevents the return from becoming an accepted result. The event buffer is printed later for readability; its order still records the actual opening and closing during the loader call.
Test changes and boundaries
Exercise all combinations of read failure and close failure with valid, malformed, and out-of-range text. Check the primary type, suppressed list, and one Opened/Closed event pair.
Accept 1 and 180 and padded valid text. Reject malformed and out-of-int-range numeric text separately from parsed domain-range failures. Do not replace rejection with zero.
Create two resources and confirm left-to-right acquisition, reverse closing, and earlier-resource cleanup when later initialization fails. Confirm the body is skipped in that last case.
Read after close and call close twice on DemoSource. Expect a closed-source error and no duplicate close event; remember idempotence is this class's documented behavior, not a universal AutoCloseable promise.
In copies, reverse related catch clauses, combine related multi-catch alternatives, reassign a multi-catch parameter, and reassign an existing resource local. Use compiler diagnostics to repair the rule violation instead of broadening catches.
Common mistakes and how to diagnose them
Catching a parent before its child
A parent catch already matches that subtype, so the later child handler is unreachable. Put NumberFormatException before IllegalArgumentException when their responses differ.
Combining related alternatives in multi-catch
A | list cannot contain a type and its subtype. Share a parent handler only when its broader coverage is intentional, or keep ordered specific catches.
Assuming a completed return means closing succeeded
Automatic closing happens before the caller receives the result. A close-only failure cancels a pending successful return, so print acceptance after the loader call completes.
Reading only getCause and missing cleanup evidence
Suppressed cleanup failures are available through getSuppressed, not automatically through getCause. A primary error may have either, both, or neither.
Expecting an unsuccessfully constructed resource to be closed by the header
Only successfully acquired earlier resources can be closed there. Constructors must manage internal partial acquisition when they fail before returning a resource.
Closing a resource you do not own
An available reference is not permission to close shared state. Agree on ownership, wrapper behavior, and post-close usage rather than blindly wrapping every parameter in try-with-resources.
Frequently asked questions
When should I use separate catches instead of multi-catch?
Use separate handlers when the caller needs different responses, such as correcting numeric text versus investigating I/O. Use multi-catch for unrelated types only when the response really is shared.
What can go in a try-with-resources header?
A suitable AutoCloseable resource declaration, or in this Java 21 baseline an existing final or effectively final local reference. Merely having a method named close without the required type contract is not enough.
Does close run after the attached catch?
No. Automatic closing is attempted before the attached catch and finally clauses run. This lesson buffers events and prints them later, which must not be confused with execution order.
What if both the body and close throw?
Normally the body failure remains primary and the cleanup failure is attached to its suppressed list. If only close fails, its failure becomes primary rather than being attached to a successful return.
Does AutoCloseable guarantee that close is idempotent?
No. DemoSource deliberately treats subsequent close calls as no-ops, but other resource types have their own contracts. Do not duplicate ownership or assume universal repeated-close safety.
Are these examples opening real files?
No. NamedResource and DemoSource are synthetic in-memory objects used to make failure and lifetime rules reproducible. The following files lessons introduce actual filesystem APIs separately.
Handle by meaning, close by ownership, preserve the evidence
Specific handlers precede compatible parents. Multi-catch shares a response for unrelated alternatives. An owned resource scope attempts cleanup in reverse acquisition order, including after body or later-initialization failure. Primary, caused, and suppressed failures are different diagnostic relationships.
Explain why a close-only failure prevents acceptance, why a body failure stays primary when close also fails, and why a reference remaining in scope does not imply an open resource. Next: files and paths, where you will create and remove temporary filesystem items deliberately.
KNOWLEDGE CHECK
Can you follow acquisition, failure, and cleanup?
Answer six questions and read the explanations. Quiz answers are temporary and do not save course progress.
References and related learning
These examples, explanations, and exercises are original SovranCode material. The official Java 21 references document exception handlers and resource lifetime rules.