{"record":{"id":"5beebae9da84e915","repo":"tursodatabase/turso","slug":"step-returned-null-which-is-only-returned-when","errorCode":null,"errorMessage":"step() returned null, which is only returned when an error occurs","messagePattern":"step\\(\\) returned null, which is only returned when an error occurs","errorType":"exception","errorClass":"SQLException","httpStatus":null,"severity":"error","filePath":"bindings/java/src/main/java/tech/turso/core/TursoStatement.java","lineNumber":53,"sourceCode":"\n  public TursoResultSet getResultSet() {\n    return resultSet;\n  }\n\n  /**\n   * Expects a clean statement created right after prepare method is called.\n   *\n   * @return true if the ResultSet has at least one row; false otherwise.\n   */\n  public boolean execute() throws SQLException {\n    resultSet.next();\n    return resultSet.hasLastStepReturnedRow();\n  }\n\n  TursoStepResult step() throws SQLException {\n    final TursoStepResult result = step(this.statementPointer);\n    if (result == null) {\n      throw new SQLException(\"step() returned null, which is only returned when an error occurs\");\n    }\n\n    return result;\n  }\n\n  /**\n   * Because turso supports async I/O, it is possible to return a {@link TursoStepResult} with\n   * {@link TursoStepResult#STEP_RESULT_ID_ROW}. However, this is handled by the native side, so you\n   * can expect that this method will not return a {@link TursoStepResult#STEP_RESULT_ID_ROW}.\n   */\n  @Nullable\n  private native TursoStepResult step(long stmtPointer) throws SQLException;\n\n  /**\n   * Throws formatted SQLException with error code and message.\n   *\n   * @param errorCode Error code.\n   * @param errorMessageBytes Error message.","sourceCodeStart":35,"sourceCodeEnd":71,"githubUrl":"https://github.com/tursodatabase/turso/blob/bad083fafbefdeae9a42ec19bdaaad8918dcf411/bindings/java/src/main/java/tech/turso/core/TursoStatement.java#L35-L71","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","solutions":["Never close the TursoDB/connection/statement while its ResultSet is still stepping — finish or abandon iteration first","Use one statement per thread; synchronize access if sharing is unavoidable","Catch SQLException around iteration and include the SQL in your error context (TursoStatement.toString() carries it)","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"],"exampleFix":"// before\nnew Thread(() -> { try { while (rs.next()) { emit(rs); } } catch (SQLException ignored) {} }).start();\nconn.close(); // races with iteration -> step() returned null\n\n// after\ntry {\n  while (rs.next()) { emit(rs); }\n} catch (SQLException e) {\n  throw new IllegalStateException(\"step failed for: \" + sql, e);\n} finally {\n  conn.close(); // close only after iteration completes","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  while (rs.next()) {\n    emit(rs);\n  }\n} catch (SQLException e) {\n  if (e.getMessage() != null && e.getMessage().contains(\"step() returned null\")) {\n    // native error path: statement likely invalidated (closed DB/statement) or\n    // shared across threads. Re-prepare the statement and retry once; if it\n    // recurs with a single-threaded, open connection, report upstream.\n  } else {\n    throw e;\n  }\n}","preventionTips":["Close statements, connections, and the DB only after iteration finishes","Keep each statement on one thread; do not share TursoStatement instances","Attach the SQL (TursoStatement.toString()) to step failures for reproducibility"],"tags":["java","jni","native","step","lifecycle","bindings"],"backgroundTag":"native-step-error","analyzedSha":"bad083fafbefdeae9a42ec19bdaaad8918dcf411","analyzedAt":"2026-08-16T23:12:11.798Z","schemaVersion":2},"datasetVersion":"2026-08-17T04:17:16.089Z"}