A failure in resource cleanup can explain why an export file is incomplete, yet the first exception visible to a caller may describe a different operation. Java's try-with-resources keeps a body failure as the primary exception and attaches cleanup failures as suppressed exceptions. Inspecting only the primary message loses useful diagnostic evidence.
Prerequisites
Use JDK 21 with javac and java available on your path. The program needs no dependencies and opens no files or sockets. Its custom AutoCloseable resource deliberately throws during close, making exception ordering repeatable without depending on an actual storage failure. The explicit require helper always runs; the checks do not need the JVM's optional assertion flag.
Two resources are declared in the first try block. The body throws an IOException named work. Closing the second resource fails, followed by closing the first. The catch block verifies that the body failure remains primary and that both cleanup failures remain available in reverse declaration order. A separate block shows what happens when the body completes normally but closing the sole resource fails.
Complete example
Save as SuppressedDemo.java.
import java.io.IOException;
import java.util.Arrays;
public class SuppressedDemo {
static final class Resource implements AutoCloseable {
private final String name;
Resource(String name) { this.name = name; }
@Override public void close() throws IOException {
throw new IOException("close-" + name);
}
}
static void require(boolean value) {
if (!value) throw new AssertionError("check failed");
}
public static void main(String[] args) {
try (var first = new Resource("first");
var second = new Resource("second")) {
throw new IOException("work");
} catch (IOException e) {
var messages = Arrays.stream(e.getSuppressed())
.map(Throwable::getMessage).toList();
require(e.getMessage().equals("work"));
require(messages.equals(java.util.List.of("close-second", "close-first")));
System.out.println("primary=" + e.getMessage());
System.out.println("suppressed=" + String.join(",", messages));
}
try (var only = new Resource("only")) {
// Successful body; closing still fails.
} catch (IOException e) {
require(e.getMessage().equals("close-only"));
require(e.getSuppressed().length == 0);
System.out.println("successful-body primary=" + e.getMessage());
}
}
}
Run and expected output
javac SuppressedDemo.java
java SuppressedDemoprimary=work
suppressed=close-second,close-first
successful-body primary=close-only
Read the exception structure
The first two output lines belong to the same exception object. The work failure is primary; close-second and close-first are suppressed, not replacement causes. The third line shows the successful-body case: its close failure becomes primary and has no suppressed siblings. These checks exercise Java's actual compiler-generated cleanup behavior rather than a manually constructed exception list.
The Java Language Specification defines try-with-resources execution, and Throwable documents the suppressed-exception API. In application code, pass the throwable to a logger that records its stack trace and exception structure. Printing only getMessage() is inadequate for diagnosis. This small demonstration prints messages to make ordering easy to inspect; it is not a complete logging implementation.
Edge cases and troubleshooting
A cause and a suppressed exception represent different relationships. The cause generally explains why the primary exception arose; suppression preserves a separate failure that could not become primary. Do not replace the cause with the first suppressed exception when wrapping a failure. If you throw a new application exception, retain the original exception as its cause so its cleanup evidence remains reachable.
There may be no suppressed exceptions when cleanup succeeds, when a resource was not successfully initialized, or when suppression has been disabled on a custom throwable. Resource-construction failures and disabled suppression are not exercised here. Avoid assuming the array always has two elements. If the source fails to compile, confirm the public class filename and JDK selected by both commands. The test validates ordering and primary selection with synthetic resources; it does not validate a database driver's cleanup behavior or any logging backend.
Primary references
Continue with Java tutorials.