How do I use SQLSTATE error codes in JDBC?

SQLState error codes are a standardized way in JDBC to categorize database errors, making it easier to handle exceptions programmatically. They’re part of the SQLException class and follow the SQL:2003 standard (like “23000” for integrity constraints). Unlike vendor-specific error codes (from getErrorCode()), SQLState is more portable across databases.

1. What is SQLState?

  • It’s a 5-character string returned by the database driver.
  • The first two characters indicate the error class (e.g., “08” for connection issues, “23” for integrity violations).
  • The last three are a subclass for more details.
  • Common examples:
    • “08001”: Can’t connect to the database.
    • “23000”: Integrity constraint violation (e.g., duplicate key).
    • “40001”: Serialization failure (often retryable in transactions).
    • “42S02”: Table not found (syntax-related).

This helps you write database-agnostic error handling, though some drivers add vendor twists.

2. Accessing SQLState in Code

When you catch an SQLException, simply call e.getSQLState(). It’s always a good idea to log or inspect both SQLState and the vendor code (e.getErrorCode()) for full context.

Here’s a basic example in Java:

package org.kodejava.jdbc;

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

public class JdbcErrorHandlingExample {
    public void executeQuery(Connection conn, String sql) {
        try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
            pstmt.executeUpdate();
        } catch (SQLException e) {
            String sqlState = e.getSQLState();
            int errorCode = e.getErrorCode();
            System.out.println("Error: SQLState=" + sqlState + ", Vendor Code=" + errorCode);
            System.out.println("Message: " + e.getMessage());

            // Handle based on SQLState
            if (sqlState != null && sqlState.startsWith("23")) {
                // Integrity issue: e.g., duplicate entry – notify user or retry
                throw new IllegalArgumentException("Data integrity violation!", e);
            } else if (sqlState != null && sqlState.startsWith("08")) {
                // Connection problem: maybe retry or fail over
                throw new RuntimeException("Connection failed!", e);
            } else {
                // Generic handling
                throw new RuntimeException("Database error occurred!", e);
            }
        }
    }
}

3. Best Practices for Using SQLState

  • Conditional Logic: Use it sparingly for decisions like retries (e.g., “40001” often means a deadlock—safe to retry the transaction).
  • Chaining Exceptions: SQLExceptions can chain (via getNextException()). Walk the chain to check all SQLStates.
  • Logging: Always include SQLState in logs for easier debugging. In modern Java (like SDK 25), use try-with-resources to auto-close resources without leaks.
  • Portability Caveat: Not all drivers implement SQLState perfectly—test against your DB (e.g., MySQL, PostgreSQL).
  • Avoid Over-Reliance: Combine with getErrorCode() for vendor-specific details, but prefer SQLState for cross-DB code.

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)

How do I handle SQL exceptions in JDBC properly?

Handling SQLException “properly” in JDBC is mostly about (1) not leaking resources, (2) preserving diagnostic detail, (3) rolling back safely, and (4) translating errors into something meaningful at your app boundary.

1) Always close JDBC resources (use try-with-resources)

This eliminates most error-handling bugs (leaks and double-closes), and it also handles exceptions thrown during close() by attaching them as suppressed exceptions.

package org.kodejava.jdbc;

import javax.sql.DataSource;
import java.sql.*;

public final class JdbcExample {
    public static void runQuery(DataSource ds, long id) {
        String sql = "select name from users where id = ?";

        try (Connection con = ds.getConnection();
             PreparedStatement ps = con.prepareStatement(sql)) {

            ps.setLong(1, id);

            try (ResultSet rs = ps.executeQuery()) {
                if (rs.next()) {
                    String name = rs.getString(1);
                    // use name...
                }
            }

        } catch (SQLException e) {
            throw toDataAccessException("Failed running query: " + sql, e);
        }
    }

    static RuntimeException toDataAccessException(String message, SQLException e) {
        // Keep the SQLException as the cause so details are not lost.
        return new RuntimeException(message + " (SQLState=" + e.getSQLState() + ", code=" + e.getErrorCode() + ")", e);
    }
}

Key points

  • Prefer PreparedStatement over Statement (safety + plan reuse).
  • Wrap/translate once, near your data-access boundary, but keep e as the cause.

2) Log/inspect the full JDBC error chain (and suppressed exceptions)

JDBC drivers can chain multiple exceptions (e.g., one per batch item), and try-with-resources can add suppressed exceptions from close().

package org.kodejava.jdbc;

import java.sql.SQLException;

public final class SqlDiagnostics {
    public static String describe(SQLException e) {
        StringBuilder sb = new StringBuilder();
        for (Throwable t = e; t != null; t = (t instanceof SQLException se) ? se.getNextException() : null) {
            if (t instanceof SQLException se) {
                sb.append("SQLException: message=").append(se.getMessage())
                        .append(", SQLState=").append(se.getSQLState())
                        .append(", code=").append(se.getErrorCode())
                        .append('\n');
            } else {
                sb.append("Throwable: ").append(t).append('\n');
            }

            for (Throwable sup : t.getSuppressed()) {
                sb.append("  suppressed: ").append(sup).append('\n');
            }
        }
        return sb.toString();
    }
}

When this matters

  • Batch updates (BatchUpdateException)
  • Failures during resource cleanup (network drop during close())

3) Handle transactions: commit/rollback with a safe rollback path

If you manually manage transactions (setAutoCommit(false)), your exception handling must:

  1. rollback on failure,
  2. not mask the original exception if rollback also fails,
  3. restore state if you’re reusing connections (pools usually reset, but don’t rely on it blindly).
package org.kodejava.jdbc;

import java.sql.*;

public final class JdbcTxExample {
    public static void transfer(Connection con, long fromId, long toId, long amount) throws SQLException {
        boolean oldAutoCommit = con.getAutoCommit();
        con.setAutoCommit(false);

        try (PreparedStatement debit = con.prepareStatement("update acct set bal = bal - ? where id = ?");
             PreparedStatement credit = con.prepareStatement("update acct set bal = bal + ? where id = ?")) {

            debit.setLong(1, amount);
            debit.setLong(2, fromId);
            debit.executeUpdate();

            credit.setLong(1, amount);
            credit.setLong(2, toId);
            credit.executeUpdate();

            con.commit();

        } catch (SQLException e) {
            try {
                con.rollback();
            } catch (SQLException rb) {
                e.addSuppressed(rb); // keep original, attach rollback failure for debugging
            }
            throw e; // or translate here
        } finally {
            try {
                con.setAutoCommit(oldAutoCommit);
            } catch (SQLException ac) {
                // typically log; don't hide earlier failure
            }
        }
    }
}

4) Don’t swallow exceptions; translate them at the right layer

Common strategy:

  • DAO/repository layer: catch SQLException, add context (operation + key parameters), then rethrow as:
    • a checked app exception (if you want callers to handle), or
    • a runtime “data access” exception (common in service-oriented apps).
  • Service/controller boundary: map to user-safe messages (avoid exposing SQL text / internals).

Avoid:

  • catch (SQLException e) {} (silences failures)
  • Throwing a new exception without e as cause (loses SQLState/vendor code)

5) Use SQLState / vendor codes for decisions (sparingly)

If you need conditional handling (e.g., unique constraint violation), prefer SQLState classes when possible:

  • 23*** integrity constraint violation (many DBs)
  • 40*** transaction rollback / serialization failure (often retryable)

But keep it minimal: drivers/databases vary.

6) Be careful with retries

Only retry when you can justify it:

  • transient network issues
  • deadlocks / serialization failures (often safe to retry the whole transaction)

Never retry blindly on all SQLExceptions.


Quick checklist

  • try-with-resources for Connection/Statement/ResultSet
  • Keep SQLException as the cause; don’t lose SQLState / error code
  • Walk getNextException() and check suppressed exceptions
  • In manual transactions: rollback in catch, attach rollback failures via addSuppressed
  • Translate exceptions at the repository boundary; expose safe messages at the edge

How do I handle exceptions and errors in Jakarta Servlets?

Handling exceptions and errors in Jakarta Servlets involves several approaches, ranging from defining error-handling configurations in the deployment descriptor (web.xml) to using annotations and specific coding practices. Below are the typical ways to handle errors and exceptions in Jakarta Servlets:


1. Using web.xml for Declarative Exception Handling

In the web.xml file, you can define error pages that are mapped to specific exceptions or HTTP status codes. When an error or exception occurs, the corresponding error page is displayed.

Example:

<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
         version="6.0">
    <!-- Define an error page for a specific HTTP status code -->
    <error-page>
        <error-code>404</error-code>
        <location>/error-404.jsp</location>
    </error-page>

    <!-- Define an error page for a specific exception -->
    <error-page>
        <exception-type>java.lang.Exception</exception-type>
        <location>/error-exception.jsp</location>
    </error-page>
</web-app>
  • error-code: Maps a specific HTTP status code (e.g., 404) to an error page.
  • exception-type: Maps a specific Java exception to an error page.
  • location: Specifies the location of the JSP or HTML page to display.

2. Using @WebServlet and Exception Handling in Code

In your custom servlet, you can explicitly handle exceptions using try-catch blocks or override the service or doGet/doPost methods.

Example:

package org.kodejava.servlet;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;

@WebServlet(urlPatterns = "/exceptionServlet", asyncSupported = true)
public class ExceptionServlet extends HttpServlet {

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        try {
            // Servlet logic that may throw an exception
            int result = 10 / 0;  // This will throw an ArithmeticException
        } catch (ArithmeticException e) {
            // Handle specific exceptions
            request.setAttribute("errorMessage", "An error occurred: " + e.getMessage());
            request.getRequestDispatcher("/error.jsp").forward(request, response);
        } catch (Exception e) {
            // Handle generic exceptions
            throw new ServletException("An unexpected error occurred", e);
        }
    }
}

3. Custom Error Pages in JSP

Error pages can be written as JSPs (.jsp). To make use of the exception details, JSPs have access to the exception and other implicit objects such as request and response.

Example: error-exception.jsp

<%@ page isErrorPage="true" %>
<html>
<head><title>Error Page</title></head>
<body>
    <h1>An error occurred</h1>
    <p>Error message: ${exception.message}</p>
    <p>Exception type: ${exception.class.name}</p>
</body>
</html>
  • The attribute isErrorPage="true" makes the JSP aware of the exception object.

4. Defining an Error Filter

You can define a Jakarta Servlet filter that intercepts all requests and handles errors more generically before they reach the targeted servlet or after they are processed.

Example:

package org.kodejava.servlet;

import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.annotation.WebFilter;

import java.io.IOException;

@WebFilter("/*")
public class ErrorHandlingFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        try {
            chain.doFilter(request, response); // Pass request and response to the next filter or servlet
        } catch (Exception e) {
            request.setAttribute("errorMessage", e.getMessage());
            request.getRequestDispatcher("/error.jsp").forward(request, response);
        }
    }

    @Override
    public void init(FilterConfig filterConfig) {
    }

    @Override
    public void destroy() {
    }
}

This filter catches any unhandled exceptions and forwards the request to an error page.


5. Logging Exceptions

It is a best practice to log exceptions for debugging and monitoring purposes. You can use logging frameworks like Java’s java.util.logging, Log4j, or SLF4J to capture exceptions.

Example with java.util.logging:

package org.kodejava.servlet;

import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.util.logging.Logger;

@WebServlet(urlPatterns = "/loggingServlet", asyncSupported = true)
public class LoggingServlet extends HttpServlet {
    private static final Logger logger = Logger.getLogger(LoggingServlet.class.getName());

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        try {
            // Code that might throw exceptions
        } catch (Exception e) {
            logger.severe("An unexpected error occurred: " + e.getMessage());
            throw new ServletException("Internal server error", e);
        }
    }
}

6. Using HTTP Status Codes

Always set appropriate HTTP status codes when handling errors, so clients are aware of the issue.

Example:

response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid input provided");

Best Practices:

  1. Consistent Error Pages: Create a unified style for error pages to provide a better user experience.
  2. Avoid Revealing Sensitive Data: Never display stack traces or sensitive information to the user.
  3. Use Centralized Logging: Implement a centralized mechanism to log and monitor exceptions.
  4. Handle Specific Errors: Provide user-friendly error messages for known and predictable failures (e.g., 404 Not Found, 403 Forbidden).
  5. Fallback Handling: Ensure a fallback mechanism is in place for uncaught exceptions (e.g., generic error page).

By combining these mechanisms, you can effectively handle exceptions and errors in Jakarta Servlets while ensuring robust error handling and user-friendly responses.


Maven dependencies

<dependency>
    <groupId>jakarta.servlet</groupId>
    <artifactId>jakarta.servlet-api</artifactId>
    <version>6.1.0</version>
    <scope>provided</scope>
</dependency>

Maven Central

How do I handle exceptions in Stream.forEach() method?

When using Java’s Stream.forEach() method, you might encounter checked exceptions. Checked exceptions can’t be thrown inside a lambda without being caught because of the Consumer functional interface. It does not allow for this in its method signature.

Here is an example of how you might deal with an exception in a forEach operation:

package org.kodejava.basic;

import java.util.List;

public class ForEachException {
    public static void main(String[] args) {
        List<String> list = List.of("Java", "Kotlin", "Scala", "Clojure");
        list.stream().forEach(item -> {
            try {
                // methodThatThrowsExceptions can be any method that throws a 
                // checked exception
                methodThatThrowsExceptions(item);
            } catch (Exception e) {
                e.printStackTrace();
            }
        });
    }

    public static void methodThatThrowsExceptions(String item) throws Exception {
        // Method implementation
    }
}

In the above code, we have a method methodThatThrowsExceptions that can throw a checked exception. In the forEach operation, we have a lambda in which we use a try-catch block to handle potential exceptions from methodThatThrowsExceptions.

However, this approach is not generally recommended because it suppresses the exception where it occurs and doesn’t interrupt the stream processing. If you need to properly handle the exception and perhaps stop processing, you may need to use a traditional for-or-each loop.

There are several reasons why exception handling within lambda expressions in Java Streams is not generally recommended.

  1. Checked Exceptions: Lambda expressions in Java do not permit checked exceptions to be thrown, so you must handle these within the lambda expression itself. This often results in bloated, less readable lambda expressions due to the necessity of a try-catch block.

  2. Suppressed Exceptions: If you catch the exception within the lambda and print the stack trace – or worse, do nothing at all – the exception is effectively suppressed. This could lead to silent failures in your code, where an error condition is not properly propagated up the call stack. This can make it harder to debug issues, as you may be unaware an exception has occurred.

  3. Robust Error Handling: Handling the exception within the lambda expression means you’re dealing with it right at the point of failure, and it might not be the best place to handle the exception. Often, you’ll want to stop processing the current operation when an exception occurs. Propagate the error up to a higher level in your software where it can be handled properly (e.g., by displaying an error message to the user, logging the issue, or retrying the operation).

  4. Impure Functions: By handling exceptions (a side effect) within lambda expressions, we are making them impure functions – i.e., functions that modify state outside their scope or depend on state from outside their scope. This goes against the principles of functional programming.

In summary, while you can handle exceptions within forEach lambda expressions in Java, doing so can create challenges in how the software handles errors, potentially leading to suppressed exceptions, less readable code, and deviations from functional programming principles. Better approaches often are to handle exceptions at a higher level, use optional values, or use features from new versions of Java (like CompletableFuture.exceptionally) or third-party libraries designed to handle exceptions in functional programming contexts.