denoland/deno · error · Error
Cannot get cursor before first iteration
Error message
Cannot get cursor before first iteration
What it means
KvListIterator.cursor exposes the opaque resumption cursor only after the first batch has been pulled — the iterator installs its internal cursor generator when the first next() resolves. Reading .cursor on a fresh, un-advanced iterator throws Error('Cannot get cursor before first iteration'). After iteration starts, the cursor encodes the last-delivered key so a later list() call can resume.
Solutions
- Advance once before reading: `await it.next()` — the cursor is valid afterwards, even if that call returned done.
- In for-await loops, read the cursor after the loop finishes (it is set by then).
- Model pagination as: iterate a batch to completion, then persist the cursor for the next invocation.
Example fix
// before
const it = kv.list({ prefix: ["users"] });
const cursor = it.cursor; // Error: Cannot get cursor before first iteration
// after
const it = kv.list({ prefix: ["users"] });
for await (const entry of it) { /* ... */ }
const cursor = it.cursor; // resumption point after the last yielded key Defensive patterns
Strategy: validation
Validate before calling
async function listPage(kv: Deno.Kv, selector: Deno.KvListSelector, cursor?: string) {
const it = kv.list(selector, cursor ? { cursor } : {});
const entries: Deno.KvEntry<unknown>[] = [];
for await (const e of it) entries.push(e); // advancing sets the cursor
return { entries, nextCursor: it.cursor }; // safe: iteration happened
} Type guard
const hasStarted = (it: Deno.KvListIterator<unknown>): boolean => {
try {
it.cursor;
return true;
} catch {
return false;
}
}; Try / catch
let cursor: string;
try {
cursor = it.cursor;
} catch (e) {
if (e instanceof Error && e.message === "Cannot get cursor before first iteration") {
await it.next(); // advance once, then read
cursor = it.cursor;
} else throw e;
} Prevention
- Never read .cursor before the first next() or for-await iteration
- Persist cursors only after finishing a batch, from a single pagination helper
- Read the cursor after the for-await loop completes — it is set by then
When it happens
Trigger: `const it = kv.list(sel); it.cursor;` — inspecting, logging, or persisting the cursor before any `await it.next()` or for-await loop has advanced the iterator.
Common situations: Pagination scaffolds that store the cursor immediately after creating the iterator; refactors that move the cursor read above the iteration loop; debugging with console.log(it.cursor).
Related errors
- batchSize must be a positive integer: received
- Limit must be a positive integer: received
- Selector can not specify both 'start' and 'end' key when…
- Selector must specify either 'prefix' or both 'start' and…
- Delay cannot be greater than 30 days: received
AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20).
Data as JSON: /api/errors/95f7a2fc2b9d5fe6.
Report an issue: GitHub.
Appendix: source
Thrown at ext/kv/01_db.ts:904
this.#selector = { start, end };
} else {
throw new TypeError(
"Selector must specify either 'prefix' or both 'start' and 'end' key",
);
}
}
ObjectFreeze(this.#selector);
this.#pullBatch = pullBatch;
this.#limit = limit;
this.#reverse = reverse;
this.#consistency = consistency;
this.#batchSize = batchSize;
this.#cursorGen = cursor ? () => cursor : null;
}
get cursor(): string {
if (this.#cursorGen === null) {
throw new Error("Cannot get cursor before first iteration");
}
return this.#cursorGen();
}
async next(): Promise<IteratorResult<Deno.KvEntry<unknown>>> {
// Fused or limit exceeded
if (
this.#done ||
(this.#limit !== undefined && this.#count >= this.#limit)
) {
return { done: true, value: undefined };
}
// Attempt to fill the buffer
if (!this.#entries?.length && !this.#lastBatch) {
const batch = await this.#pullBatch(
this.#selector,View on GitHub (pinned to 9ad36f7a2c)