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

  1. Advance once before reading: `await it.next()` — the cursor is valid afterwards, even if that call returned done.
  2. In for-await loops, read the cursor after the loop finishes (it is set by then).
  3. 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

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


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)