tursodatabase/turso · error · java.sql.SQLException

step() returned null, which is only returned when an error…

Error message

step() returned null, which is only returned when an error occurs

What it means

TursoStatement.step() wraps the native step(long) and treats a null return as an error, because the JNI contract returns null only on failure. Unlike the ResultSet 'invalid result' errors, no result object or engine message accompanies it — this is a bare 'native call failed while advancing the statement'. Root causes are engine-level failures or stepping a statement whose pointer is stale (already closed, finalized, or reset underneath you).

Solutions

  1. Catch SQLException at the iteration boundary and re-prepare the statement once on a fresh handle if the operation is idempotent
  2. Verify the statement and connection are still open before stepping, and never step concurrently from two threads
  3. If the failure repeats deterministically, check the database file and disk health with the sqlite3 CLI

Example fix

// before
while (rs.next()) { /* rows */ } // 'step() returned null...' mid-loop

// after
try {
  while (rs.next()) { /* rows */ }
} catch (SQLException e) {
  stmt.close();
  stmt = conn.prepareStatement(sql); // statement unusable: re-prepare, retry once
  rs = stmt.executeQuery(sql);
}
Defensive patterns

Strategy: try-catch

Try / catch

try {
  while (rs.next()) { /* ... */ }
} catch (SQLException e) {
  // null native return: the statement is unusable
  stmt.close();
  stmt = conn.prepareStatement(sql); // re-prepare and re-execute once
}

Prevention

When it happens

Trigger: Stepping after the statement was closed; stepping while another thread closes the connection or finalizes the statement; an engine I/O failure during iteration; native memory exhaustion.

Common situations: Lifecycle races with pooled connections; statements kept alive longer than their database handle; iterating large result sets while a timeout handler tears the connection down.

Related errors


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

Appendix: source

Thrown at bindings/java/src/main/java/tech/turso/core/TursoStatement.java:53

  public TursoResultSet getResultSet() {
    return resultSet;
  }

  /**
   * Expects a clean statement created right after prepare method is called.
   *
   * @return true if the ResultSet has at least one row; false otherwise.
   */
  public boolean execute() throws SQLException {
    resultSet.next();
    return resultSet.hasLastStepReturnedRow();
  }

  TursoStepResult step() throws SQLException {
    final TursoStepResult result = step(this.statementPointer);
    if (result == null) {
      throw new SQLException("step() returned null, which is only returned when an error occurs");
    }

    return result;
  }

  /**
   * Because turso supports async I/O, it is possible to return a {@link TursoStepResult} with
   * {@link TursoStepResult#STEP_RESULT_ID_ROW}. However, this is handled by the native side, so you
   * can expect that this method will not return a {@link TursoStepResult#STEP_RESULT_ID_ROW}.
   */
  @Nullable
  private native TursoStepResult step(long stmtPointer) throws SQLException;

  /**
   * Throws formatted SQLException with error code and message.
   *
   * @param errorCode Error code.
   * @param errorMessageBytes Error message.

View on GitHub (pinned to 244cde92a7)