tursodatabase/turso · error · SQLException

step() returned invalid result: " + errorMessage

Error message

step() returned invalid result: " + errorMessage

What it means

While iterating, statement.step() returned a TursoStepResult in an invalid state — a native execution error surfaced mid-iteration. The native error text is appended to the message. next() marks the set closed (open = false) before throwing, so the ResultSet is unusable afterwards.

Source

Thrown at bindings/java/src/main/java/tech/turso/core/TursoResultSet.java:95

    if (isEmptyResultSet || pastLastRow) {
      return false; // completed ResultSet
    }

    if (maxRows != 0 && row == maxRows) {
      return false;
    }

    lastStepResult = this.statement.step();
    log.debug("lastStepResult: {}", lastStepResult);
    if (lastStepResult.isRow()) {
      row++;
    }

    if (lastStepResult.isInInvalidState()) {
      open = false;
      String errorMessage = lastStepResult.getErrorMessage();
      if (errorMessage != null && !errorMessage.isEmpty()) {
        throw new SQLException("step() returned invalid result: " + errorMessage);
      } else {
        throw new SQLException("step() returned invalid result: " + lastStepResult);
      }
    }

    pastLastRow = lastStepResult.isDone();
    if (pastLastRow && row == 0) {
      isEmptyResultSet = true;
    }
    return !pastLastRow;
  }

  /** Checks whether the last step result has returned row result. */
  public boolean hasLastStepReturnedRow() {
    return lastStepResult != null && lastStepResult.isRow();
  }

  /** Checks whether the cursor is positioned after the last row. */

View on GitHub (pinned to bad083fafb)

Solutions

  1. Read the appended native message — it carries the real cause (constraint name, 'database is locked', ...)
  2. Fix the root cause: relax/repair constraints, use INSERT OR IGNORE / ON CONFLICT where semantics allow, set a busy timeout or retry on lock errors
  3. Abort the batch on this exception — the ResultSet is already closed and cannot resume
  4. Avoid calling interrupt() on a connection whose statements are being iterated

Example fix

// before
while (rs.next()) { emit(rs); } // dies mid-batch on UNIQUE violation

// after
try {
  while (rs.next()) { emit(rs); }
} catch (SQLException e) {
  // e.getMessage() ends with the native cause, e.g. constraint failure
  abortBatchAndLog(e);
}
// and make conflicts explicit in SQL:
// INSERT INTO t VALUES(?, ?) ON CONFLICT(id) DO NOTHING
Defensive patterns

Strategy: try-catch

Try / catch

try {
  while (rs.next()) {
    emit(rs);
  }
} catch (SQLException e) {
  // message ends with the native cause (constraint name, 'database is locked', ...)
  abortBatch(e.getMessage());
  // rs is now closed — re-execute from the last checkpoint if you need to resume
}

Prevention

When it happens

Trigger: Stepping a query whose evaluation fails partway: a UNIQUE/NOT NULL/CHECK constraint violation on an INSERT/UPDATE driven through step(), SQLITE_BUSY on a locked database, interrupt() from another thread, or an I/O error while advancing the cursor.

Common situations: Batch INSERT loops hitting a duplicate key mid-batch; concurrent writers holding the lock without busy_timeout; cancellation logic interrupting a running query; database files on failing/removable storage.

Related errors


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