We are still cooking the magic in the way!
Handling SQL Exceptions & Resources
Handling SQL Exceptions & Resources
Two of the most common sources of bugs in JDBC code are unclosed resources and swallowed exceptions. A Connection, PreparedStatement, or ResultSet that is never closed leaks file descriptors and database-side cursors, degrading or crashing the application under load. An exception that is caught and silently discarded turns a clear error into a mystery. This lesson covers the right patterns — and the real trade-offs — so your DAO layer is robust in production.
What SQLException Carries
java.sql.SQLException is a checked exception that wraps three distinct pieces of diagnostic information beyond the message string:
- SQL State — a five-character code defined by the SQL standard (X/Open or ISO). The first two characters identify the class of error:
08= connection failure,22= data exception,23= integrity constraint violation,42= syntax error or access violation. - Error Code — a vendor-specific integer (e.g. MySQL
1062= duplicate entry, PostgreSQL0if unknown). Useful when you need database-specific recovery. - Chained exceptions —
getNextException()returns additionalSQLExceptions that the driver chained to the first. Always iterate the chain when logging.
23000 or 23505 rather than vendor error codes, which differ between databases.
Subclasses of SQLException Worth Knowing
Since Java 6, JDBC defines several typed subclasses that let you catch specific categories without inspecting SQLState strings:
SQLTransientException— the operation may succeed if retried (e.g.SQLTransientConnectionExceptionfor a transient network glitch).SQLNonTransientException— retrying without fixing the root cause will not help (e.g. bad SQL syntax, constraint violation).SQLIntegrityConstraintViolationException— subclass ofSQLNonTransientException; thrown on unique, foreign-key, or check constraint violations.SQLTimeoutException— the query exceeded the statement timeout set viasetQueryTimeout().BatchUpdateException— thrown when one statement in aexecuteBatch()call fails;getUpdateCounts()reveals which rows succeeded.
Try-With-Resources: The Only Acceptable Pattern
Before Java 7, JDBC code required nested finally blocks to guarantee that every resource was closed even when an exception was thrown mid-operation. That code was notoriously verbose and easy to get wrong — a common mistake was to close only the ResultSet but forget the Statement. Try-with-resources eliminates that problem entirely.
Any object whose class implements java.lang.AutoCloseable can appear in the resource list of a try statement. Connection, Statement, PreparedStatement, and ResultSet all implement AutoCloseable. Resources are closed in reverse declaration order — ResultSet before PreparedStatement before Connection — regardless of whether an exception is thrown.
ResultSet is created after the statement is prepared and parameters are bound, so it cannot share the outer resource list without awkward null-initialisation. A nested try (ResultSet rs = ps.executeQuery()) is the cleanest solution and makes the lifecycle of each object crystal-clear.
Suppressed Exceptions
Try-with-resources handles a subtle edge case that the old finally pattern got wrong: if the body of the try throws an exception and close() also throws, Java attaches the close exception as a suppressed exception on the primary one rather than replacing it. This preserves the original error — the thing you actually need to diagnose — while still recording the close failure.
The Pre-Java-7 Anti-Pattern (and Why It Failed)
For historical context, here is the fragile pattern try-with-resources replaced. Notice how a single forgotten null-check or a thrown exception inside finally can mask the original error or leave a resource open:
ignored variable. At minimum, log them at WARN level. Silenced exceptions make production incidents dramatically harder to diagnose and may indicate serious problems like a pool leak or a corrupted transaction.
Wrapping SQLException in a Runtime Exception
Because SQLException is checked, it leaks database details into every layer of your application if you let it propagate unchecked. The standard DAO pattern wraps it in a custom unchecked exception before it crosses the DAO boundary:
The service layer catches DataAccessException (or lets it propagate to the servlet/controller) without depending on java.sql at all. This keeps your database technology choice hidden behind the DAO interface — a key goal of the pattern.
Setting Statement and Query Timeouts
A runaway query can hold a pool connection for minutes, starving other requests. Always set a queryTimeout on any Statement that executes user-driven logic:
Summary
Robust JDBC resource management rests on three habits: always use try-with-resources for every Connection, Statement, and ResultSet; always log the full exception chain including SQLState, error code, and suppressed exceptions; and always wrap SQLException in a domain-specific unchecked exception before it escapes the DAO layer. Apply typed subclasses (SQLIntegrityConstraintViolationException, SQLTransientConnectionException) to handle specific failure modes gracefully without parsing strings. In the next lesson you will add a service layer on top of the DAO that enforces business rules and transactional boundaries.