tursodatabase/turso · error · SQLException

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

Error message

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

What it means

TursoStatement.step() wraps the JNI call step(long); the native method returns null only on its error path, and the Java wrapper converts that null into this SQLException. It means the native engine failed while advancing the statement in a way that did not produce a TursoStepResult (the least-informative failure mode of the step machinery).

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 bad083fafb)

Solutions

  1. Never close the TursoDB/connection/statement while its ResultSet is still stepping — finish or abandon iteration first
  2. Use one statement per thread; synchronize access if sharing is unavoidable
  3. Catch SQLException around iteration and include the SQL in your error context (TursoStatement.toString() carries it)
  4. Re-run the same SQL in the CLI/tursodb to see whether the query itself errors; if it does not, report the repro — null from step() is a native diagnostics gap

Example fix

// before
new Thread(() -> { try { while (rs.next()) { emit(rs); } } catch (SQLException ignored) {} }).start();
conn.close(); // races with iteration -> step() returned null

// after
try {
  while (rs.next()) { emit(rs); }
} catch (SQLException e) {
  throw new IllegalStateException("step failed for: " + sql, e);
} finally {
  conn.close(); // close only after iteration completes
Defensive patterns

Strategy: try-catch

Try / catch

try {
  while (rs.next()) {
    emit(rs);
  }
} catch (SQLException e) {
  if (e.getMessage() != null && e.getMessage().contains("step() returned null")) {
    // native error path: statement likely invalidated (closed DB/statement) or
    // shared across threads. Re-prepare the statement and retry once; if it
    // recurs with a single-threaded, open connection, report upstream.
  } else {
    throw e;
  }
}

Prevention

When it happens

Trigger: Stepping a statement whose native execution fails outright: the DB or statement was closed/invalidated underneath (dangling statementPointer), invalid concurrent use of one statement across threads, or native error paths that return null rather than an error result.

Common situations: Closing the database or connection while a ResultSet is mid-iteration; sharing a TursoStatement between threads without synchronization; holding result sets open across connection lifecycle events; bugs in native error mapping.

Related errors


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