Throwing, Catching and Try-With-Resources
The throw Statement
throw creates and throws an exception object:
throw new IllegalArgumentException("age must be non-negative, got: " + age);
The exception object is allocated on the heap like any other Java object. The JVM captures a stack trace at the point of the throw, recording the current call stack — which methods were active, at which line numbers. This trace is stored in the exception and is invaluable for debugging.
Once thrown, the JVM begins unwinding the stack: it exits the current method (and every caller, in succession) until it finds a matching catch block. If no catch block is found in any stack frame, the thread terminates and the uncaught exception is printed to System.err.
The try-catch Block
try {
String text = Files.readString(Path.of("data.txt"));
process(text);
} catch (NoSuchFileException e) {
System.err.println("File not found: " + e.getFile());
} catch (IOException e) {
System.err.println("I/O error: " + e.getMessage());
}
Key rules:
catchblocks are checked in order. The first matching type handles the exception.- More specific types must come before more general ones.
catch (IOException e)beforecatch (Exception e)— otherwise the specific catch is unreachable and the compiler rejects the code. - The variable
ein the catch block receives a reference to the thrown exception object. You can inspect its message, cause, and stack trace.
Multi-Catch (Union Catch)
Since Java 7, you can catch multiple unrelated exception types with a single handler:
try {
doSomething();
} catch (IOException | SQLException e) {
System.err.println("Operation failed: " + e.getMessage());
}
The variable e is implicitly final in a multi-catch block — you cannot assign to it. The exception types in the union must be disjoint (no type in the list is a subtype of another in the list).
The finally Block
The finally block always runs — regardless of whether:
- The
tryblock completes normally. - The
tryblock throws an exception (caught or uncaught). - The
tryblock executes areturn,break, orcontinue.
FileInputStream fis = null;
try {
fis = new FileInputStream("data.txt");
int b;
while ((b = fis.read()) != -1) {
process((byte) b);
}
} catch (IOException e) {
System.err.println("Error: " + e.getMessage());
} finally {
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
System.err.println("Failed to close file");
}
}
}
The finally block is used for cleanup that must happen regardless — closing files, releasing locks, freeing native resources.
Important edge case: if the try block throws an exception and the finally block also throws an exception, the finally exception replaces the try exception — the original exception is lost. This is one reason try-with-resources (below) is preferred: it preserves the original exception and attaches the close exception as a suppressed exception.
Try-With-Resources (Java 7+)
Try-with-resources eliminates the verbose finally-close pattern:
try (FileReader fr = new FileReader("data.txt");
BufferedReader br = new BufferedReader(fr)) {
String line = br.readLine();
System.out.println(line);
}
Resources are closed automatically when the try block exits — even if an exception occurs. Key properties:
- Resources must implement
AutoCloseable(or the olderCloseable). - Multiple resources separated by semicolons are closed in reverse order of declaration (
brthenfr). - If the try body throws and
close()also throws, the close exception is suppressed and attached to the main exception viagetSuppressed():
try (FailingResource r = new FailingResource()) {
throw new Exception("try body failed");
} catch (Exception e) {
System.err.println("Main: " + e.getMessage());
for (Throwable suppressed : e.getSuppressed()) {
System.err.println("Suppressed: " + suppressed.getMessage());
}
}
AutoCloseable versus Closeable
AutoCloseable.close()may throwException.CloseableextendsAutoCloseable, narrowingclose()to throwIOExceptioninstead — specifically for I/O resources.
In practice, use AutoCloseable for general-purpose resources and Closeable for I/O.
The try-only Block (No catch or finally)
A try block with resources but without catch or finally is valid:
try (BufferedReader br = Files.newBufferedReader(Path.of("data.txt"))) {
String line = br.readLine();
System.out.println(line);
}
// br.close() is called automatically; if it throws, the exception propagates
This is the cleanest form when you have nothing useful to do with the checked exceptions — you simply declare them in your method’s throws clause and let them propagate.
The throws Declaration
A method declares the checked exceptions it might propagate:
void readFile(String path) throws IOException {
String text = Files.readString(Path.of(path));
process(text);
}
The throws clause is a contract with callers: “if you call this method, you must either handle IOException or propagate it yourself.” The compiler enforces this for checked exceptions only. Unchecked exceptions (subclasses of RuntimeException and Error) can be thrown without being declared.