Skip to content

Commit 96d87d3

Browse files
authored
Tighten the warning about sqlite cursor usage across awaits (#31087)
It won't actually break anything, you'll just read an inconsistent snapshot and potentially uncommitted data.
1 parent 9ca40cb commit 96d87d3

1 file changed

Lines changed: 5 additions & 7 deletions

File tree

src/content/docs/durable-objects/api/sqlite-storage-api.mdx

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -154,9 +154,9 @@ class MyDurableObject(DurableObject):
154154
A cursor (`SqlStorageCursor`) to iterate over query row results as objects. `SqlStorageCursor` is a JavaScript [Iterable](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_iterable_protocol), which supports iteration using `for (let row of cursor)`. `SqlStorageCursor` is also a JavaScript [Iterator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_iterator_protocol), which supports iteration using `cursor.next()`.
155155

156156
:::caution[Consume cursors synchronously]
157-
It is not safe to hold a reference to a `SqlStorageCursor` and continue reading from it after an `await`. When you `await` an asynchronous operation, other requests can interleave and modify the underlying database, which can silently invalidate the cursor. This does not produce an error — the cursor may appear to work but return unpredictable results.
157+
Although a cursor object can technically be held across an `await`, it does not provide a stable snapshot of the query results. A cursor resumed after an `await` may observe rows inserted, updated, or deleted after the cursor was created, including writes from a later implicit transaction that has not yet committed. If that later transaction rolls back, the cursor may have already returned data from a state that never became durable.
158158

159-
Always consume the cursor fully before yielding the event loop. Call `.toArray()`, `.one()`, or iterate with `for...of` synchronously after calling `sql.exec()`:
159+
For predictable behavior, fully consume cursors synchronously before the next `await`, for example with `.toArray()` or `Array.from(cursor)`. Treat cursors that cross `await` boundaries as having no snapshot isolation guarantees.
160160

161161
```ts
162162
// ✅ Safe: cursor is fully consumed before any await
@@ -165,15 +165,13 @@ const result = await fetch("https://example.com", { method: "POST", body: JSON.s
165165
```
166166

167167
```ts
168-
// 🔴 Unsafe: cursor is read after an await
168+
// ⚠️ No snapshot isolation: cursor may reflect changes made after it was created
169169
const cursor = this.ctx.storage.sql.exec("SELECT * FROM users");
170170
await fetch("https://example.com/notify");
171-
// Another request may have modified the database during the await above.
172-
// The cursor is now potentially invalid — it may appear to work but return wrong data.
171+
// Rows returned here may include writes that occurred during the await,
172+
// including uncommitted data from a later implicit transaction.
173173
const rows = cursor.toArray();
174174
```
175-
176-
If you need to prevent other requests from interleaving while you hold a cursor, use [`blockConcurrencyWhile()`](/durable-objects/api/state/#blockconcurrencywhile).
177175
:::
178176

179177
`SqlStorageCursor` supports the following methods:

0 commit comments

Comments
 (0)