In JDBC, how should Connection, Statement and ResultSet be closed, and why?
answer
- Something outside the JVM is being held
- The language has a construct for this
- Reverse order, and on every exception path
- Suppressed, not replaced, when close throws
basics
~20 sDeclare each of them in a try-with-resources block so they close in reverse order even when something throws. Every one holds a scarce handle — a server-side cursor or a pooled connection — and an unclosed handle is leaked until the process ends.
solid answer
~50 sAll three implement `AutoCloseable`, so the correct pattern is a single try-with-resources that declares the `Connection`, then the `PreparedStatement`, then the `ResultSet` (or nested blocks when the ResultSet comes from an execute call inside the body). Resources are closed in reverse declaration order automatically, on the success path and on every exception path, and an exception thrown by `close()` is attached to the primary exception as a suppressed exception rather than replacing it. The spec does say closing a `Statement` closes its current `ResultSet` and closing a `Connection` closes its statements, but leaning on that cascade is a bad habit: the cascade only fires when the outer object is actually closed, which is precisely what fails in leaky code. On a pooled `DataSource`, `Connection.close()` does not drop a socket — it hands the connection back to the pool, so *not* calling it removes that connection from circulation entirely.
code
java · 10 linesString sql = "SELECT id, email FROM users WHERE active = ?";
try (Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setBoolean(1, true);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) {
process(rs.getLong("id"), rs.getString("email"));
}
}
} // rs closed, then ps, then conn returned to the poolgo deeper
Write the try-with-resources shape from memory and know that all three JDBC objects belong in it. Be able to say what each one is holding on the database side.
Explain reverse-order closing, suppressed exceptions, and the cascade rules — including the asymmetry that a ResultSet close does not close its Statement — and why depending on the cascade is still wrong.
Talk about how leaks actually show up in a running service and how you find the borrow site, plus the loop case where a long-held connection accumulates cursors even though every method eventually returns.
Own the convention: data-access code never hands JDBC objects across a boundary, leak detection is on in non-production environments, and the pattern is enforced by static analysis so it does not depend on each reviewer noticing.
## What is actually being held Each JDBC object corresponds to something scarce outside the JVM. A `Connection` is a session with the database server — a socket, server memory, and in a pooled application one of a strictly limited number of slots. A `Statement` may own a server-side cursor and prepared-statement state. A `ResultSet` may own an open cursor over rows the server is still holding. None of this is reclaimed by garbage collection in any timely way; drivers may implement finalizer-like safety nets, but they are not a design you can rely on. The consequence of forgetting is not an immediate error, which is what makes it dangerous. The application works in testing and degrades in production: statement handles accumulate until the database refuses new cursors, or borrowed connections are never returned and every request eventually blocks waiting for one. ## try-with-resources is the answer Since Java 7, `Connection`, `Statement` and `ResultSet` all extend `AutoCloseable`, so the language does the work: Declare the resources in one `try (...)` header, or in nested headers when a later resource is produced by a call rather than by a plain expression. Java closes them in the **reverse** of declaration order — ResultSet first, Connection last — which is the order you want, since a cursor should be released before the session that owns it. The close calls happen on normal completion, on `return`, and on any exception. Exception handling is the subtle part. If the body throws and a `close()` also throws, the body's exception is the one propagated and the close exception is attached to it; you can read them with `Throwable.getSuppressed()`. Hand-written `finally` blocks get this wrong in the opposite direction: a naive `finally { rs.close(); }` that throws replaces the real failure with a meaningless close error, hiding the original cause. That alone is a good reason never to hand-roll the pattern any more. Before Java 7 the correct shape needed a nested `try`/`finally` per resource with a null check in each, which is why so much legacy JDBC code is a wall of boilerplate — and why so much of it is subtly wrong. ## The cascade rules, and why not to depend on them JDBC does specify cascading closes. Closing a `Statement` closes the `ResultSet` currently open on it; re-executing a `Statement` also implicitly closes its previous `ResultSet`; closing a `Connection` releases its statements. Note the asymmetry: closing a `ResultSet` does **not** close the `Statement` that produced it. The cascades are real, but they only help when the outer object is genuinely closed — and the failure mode you are guarding against is exactly the outer object *not* being closed. There is also a middle case that bites in long-running code: a method that holds one `Connection` open for minutes and creates statements inside a loop leaks a cursor per iteration, because nothing closes until the connection finally does. Close each object at the scope where you stop needing it. ## Pooled connections change the meaning of close() When the `Connection` came from a pooled `DataSource`, the object you hold is a logical handle wrapping a physical connection. Calling `close()` on it does not tear down the network connection — it marks the handle dead and returns the physical connection to the pool for the next borrower. This inverts the intuition that closing is expensive and therefore worth avoiding: with a pool, closing is cheap and *skipping* it is what costs you, because that connection is removed from the pool's inventory for as long as your code holds it. Pools typically also reset per-connection state such as auto-commit when the connection goes back, so returning it promptly is what keeps the reset honest. ## What to check in review Look for any JDBC object created outside a try-with-resources header. Look for methods that return a `ResultSet` or a `Statement` to a caller, which makes ownership ambiguous — map rows into your own objects inside the method and return those instead. Look for a `Connection` stored in a field or held across a long computation. Look for a `finally` block whose own `close()` can throw. And in a service, enable the pool's leak-detection reporting during development: a stack trace naming the borrow site is far faster than reasoning about which path forgot to close.
- What happens to an exception thrown by close() inside try-with-resources?If the block body already threw, the body's exception propagates and the close exception is attached to it as a suppressed exception, readable via Throwable.getSuppressed(). If the body completed normally, the close exception propagates on its own. A hand-written finally block would instead have discarded the original failure.
- Why must you still close a Connection borrowed from a pool, when nothing is really being disconnected?Because close() is how the logical handle is returned to the pool. Skipping it takes that physical connection out of circulation for the lifetime of your reference, and the pool's per-return state reset never runs. Leak detection in the pool reports the borrow site when a handle is held too long.
- Is it safe to return a ResultSet from a data-access method?No. The caller then owns a cursor whose statement and connection it did not create, and it cannot close them correctly. Consume the rows inside the method, map them into domain objects or a list, and return that; the JDBC objects stay inside the try-with-resources that created them.
saying these in an interview costs you the question
- Relies on garbage collection to release connections
- Closes only the Connection and calls it a day
- Puts close() in a finally block that can throw and mask the cause
- Thinks closing a ResultSet closes its Statement
- Holds one Connection in a field for the object's lifetime