tursodatabase/turso · error · SQLException

Statement is closed

Error message

Statement is closed

What it means

ensureOpen() is the shared guard at the top of JDBC4Statement's executeQuery, execute, getResultSet, addBatch, clearBatch and executeBatch (executeUpdate reaches it through execute). Once the `closed` flag is set it throws SQLException("Statement is closed"). Statements are closed both by an explicit close() and when their connection is closed, so the original close is often not visible at the failure site.

Solutions

  1. Check stmt.isClosed() before use, or recreate the statement per unit of work
  2. Create statements with try-with-resources scoped to one task; let the pool manage connections only
  3. Keep the connection open for the statement's whole lifetime and verify conn.isValid() when a pool is involved
  4. Catch the SQLException and treat 'Statement is closed' as a signal to rebuild the statement and retry once

Example fix

// before
private final Statement stmt; // cached field
public List<User> users() throws SQLException {
  return query(stmt.executeQuery("SELECT ...")); // fails after first close
}

// after
public List<User> users() throws SQLException {
  try (Statement stmt = conn.createStatement();
       ResultSet rs = stmt.executeQuery("SELECT ...")) {
    return query(rs);
  }
}
Defensive patterns

Strategy: validation

Validate before calling

if (stmt.isClosed()) {
  stmt = conn.createStatement(); // recreate and reconfigure as needed
}
ResultSet rs = stmt.executeQuery(sql);

Try / catch

try {
  return stmt.executeQuery(sql);
} catch (SQLException e) {
  if (e.getMessage() != null && e.getMessage().contains("Statement is closed")) {
    try (Statement fresh = conn.createStatement()) {
      return fresh.executeQuery(sql); // rebuild and retry once
    }
  }
  throw e;
}

Prevention

When it happens

Trigger: Reusing a statement after close(); execute/addBatch/executeBatch on a statement whose connection was closed (connection close cascades to its statements); statements stored in fields or app-level caches that outlive their lifespan; double-processing a request where the second pass hits an already-closed statement.

Common situations: App-level statement caching in servlets or service singletons; background jobs holding statements while a connection pool reclaims connections (idle timeout, eviction on validation failure); helper classes that hide statement creation but expose execute methods; tests that close resources early.

Related errors


AI-assisted analysis of tursodatabase/turso@244cde92a7 (2026-08-20). Data as JSON: /api/errors/fb71198eed972681. Report an issue: GitHub.

Appendix: source

Thrown at bindings/java/src/main/java/tech/turso/jdbc4/JDBC4Statement.java:500

  /**
   * Functional interface for SQL callable operations.
   *
   * @param <T> the return type
   */
  @FunctionalInterface
  protected interface SQLCallable<T> {
    /**
     * Executes the SQL operation.
     *
     * @return the result of the operation
     * @throws SQLException if a database access error occurs
     */
    T call() throws SQLException;
  }

  private void ensureOpen() throws SQLException {
    if (closed) {
      throw new SQLException("Statement is closed");
    }
  }
}

View on GitHub (pinned to 244cde92a7)