How do I handle SQL exceptions in JDBC for MySQL properly?

For MySQL (mysql-connector-j), “proper” SQLException handling is the same core approach as JDBC in general, plus a few MySQL-specific signals (SQLState + error code) that are worth using for translation/retry decisions.

1) Keep the important diagnostics (MySQL error code + SQLState)

MySQL gives you two invaluable fields:

  • e.getErrorCode()MySQL vendor error code (e.g., 1062 for duplicate key)
  • e.getSQLState()SQLState (often 23000, 40001, etc.)

A good pattern is: wrap once with context, but preserve those fields.

package org.kodejava.jdbc;

import java.sql.SQLException;

public final class MySqlExceptions {
    public static RuntimeException translate(String operation, SQLException e) {
        String msg = operation
                     + " failed (SQLState=" + e.getSQLState()
                     + ", errorCode=" + e.getErrorCode() + ")";

        // Keep e as the cause.
        return switch (e.getErrorCode()) {
            case 1062 ->
                    new IllegalStateException(msg + " - duplicate key", e); // unique constraint violation
            case 1213 ->
                    new IllegalStateException(msg + " - deadlock", e);      // often retryable
            case 1205 ->
                    new IllegalStateException(msg + " - lock wait timeout", e); // often retryable
            default -> new RuntimeException(msg, e);
        };
    }
}

2) MySQL error codes you’ll commonly care about

These are the ones that usually drive different handling:

Situation MySQL error code Typical SQLState What to do
Unique constraint violation (“Duplicate entry”) 1062 23000 Return “already exists” / map to 409 / domain error
Deadlock found 1213 40001 Often safe to retry the whole transaction
Lock wait timeout exceeded 1205 often 41000 Often retry or surface “please retry”
Foreign key constraint fails 1451/1452 23000 Map to domain validation (cannot delete/insert due to FK)
Connection/link failure varies 08xxx / 08S01 Treat as transient infra failure; maybe retry with backoff

Rule of thumb: prefer the vendor error code for MySQL-specific branching (it’s the most consistent), and keep SQLState for general categorization/logging.

3) Retrying safely (only for the right failures)

Only retry if:

  • the operation is idempotent, or you’re retrying the entire transaction from the beginning, and
  • the failure is one of the known transient classes (deadlock / lock timeout / connection hiccup).

A minimal “should retry?” helper:

package org.kodejava.jdbc;

import java.sql.SQLException;

public final class MySqlRetry {
    public static boolean isRetryable(SQLException e) {
        int code = e.getErrorCode();
        String state = e.getSQLState();

        // MySQL deadlock / lock wait timeout
        if (code == 1213 || code == 1205) return true;

        // Connection exception class (SQLState starts with "08")
        if (state != null && state.startsWith("08")) return true;

        return false;
    }
}

If you do retry, keep it small (e.g., 2–3 attempts) with jittered backoff, and log the final failure with the full chain (getNextException()) and suppressed exceptions.

4) Transactions: rollback without hiding the original error

With MySQL, rollback can also throw if the connection is broken. Best practice: attach rollback failure as suppressed so you don’t lose the root cause.

package org.kodejava.jdbc;

import java.sql.Connection;
import java.sql.SQLException;

public final class TxUtil {
    public static void rollbackQuietly(Connection con, SQLException original) {
        try {
            con.rollback();
        } catch (SQLException rb) {
            original.addSuppressed(rb);
        }
    }
}

5) MySQL Connector/J note: you usually don’t need Class.forName(...)

With modern JDBC drivers (including MySQL Connector/J 8+), the driver is auto-registered via the Service Provider mechanism. Calling Class.forName("com.mysql.cj.jdbc.Driver") is typically unnecessary unless you’re in a very unusual classloading environment.

6) What to log (and what not to log)

Log:

  • operation name (e.g., "insert user")
  • SQLState, error code
  • exception chain (getNextException())
  • safe parameter identifiers (e.g., user id), not secrets

Avoid logging:

  • credentials
  • sensitive values (passwords, tokens)
  • huge SQL strings with embedded data (use prepared statements so you don’t have that problem)

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.