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
- Read the appended native message — it carries the real cause (constraint name, 'database is locked', ...)
- 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
- Abort the batch on this exception — the ResultSet is already closed and cannot resume
- 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
- Design batch loops to be resumable: track the last processed row so a mid-batch failure is recoverable
- Use ON CONFLICT / OR IGNORE for expected duplicates instead of failing the step
- Configure busy behavior and avoid interrupt() while statements iterate
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
- step() returned invalid result: " + lastStepResult
- SQLite only supports TYPE_FORWARD_ONLY cursors
- SQLite only supports CONCUR_READ_ONLY cursors
- SQLite only supports closing cursors at commit
- The result set is not open
AI-assisted analysis of tursodatabase/turso@bad083fafb (2026-08-16).
Data as JSON: /api/errors/5fffdd0689bd9d65.
Report an issue: GitHub.