denoland/deno · error · TypeError

Selector must specify either 'prefix' or both 'start' and…

Error message

Selector must specify either 'prefix' or both 'start' and 'end' key

What it means

Without a prefix, a selector must define a complete range: both start and end keys. Anything else — {start} alone, {end} alone, or an empty selector {} — throws TypeError("Selector must specify either 'prefix' or both 'start' and 'end' key"). The rationale: prefix bounds the range by itself, while a half-open pair would request an unbounded scan of the keyspace.

Solutions

  1. Validate the selector shape before calling list(): require prefix or both bounds.
  2. When a half-open range is wanted, synthesize the missing bound: start defaults to [] (very first key) or end to a sentinel key above your last real key.
  3. Type selectors as a discriminated union (prefix form vs range form) so TypeScript enforces the shape.

Example fix

// before
kv.list({ start: ["2026"] }); // throws

// after
kv.list({ start: ["2026"], end: ["2027"] });
// or scope with a prefix instead:
kv.list({ prefix: ["2026"] });
Defensive patterns

Strategy: type-guard

Validate before calling

function normalizeSelector(s: Record<string, undefined | Deno.KvKey>) {
  const has = (k: string) => s[k] !== undefined;
  if (has("prefix")) return { prefix: s.prefix! };
  if (has("start") && has("end")) return { start: s.start!, end: s.end! };
  // synthesize missing bounds for half-open ranges
  if (has("start")) return { start: s.start!, end: ["\uffff"] };
  if (has("end")) return { start: [], end: s.end! };
  return { prefix: [] }; // full scan, explicitly
}

Type guard

type ListSelector =
  | { prefix: Deno.KvKey; start?: Deno.KvKey; end?: Deno.KvKey }
  | { start: Deno.KvKey; end: Deno.KvKey };

const isCompleteSelector = (s: ListSelector): boolean =>
  "prefix" in s || ("start" in s && "end" in s);

Prevention

When it happens

Trigger: `kv.list({ start: ["a"] })`; `kv.list({ end: ["z"] })`; `kv.list({})`; selectors built from optional fields where all of prefix/start/end ended up absent (undefined values are ignored by the hasOwn checks).

Common situations: Filter UIs where both bounds are optional and the user supplies only one; spreading a config that may omit keys; porting from cursor-only iteration models that have no range concept.

Related errors


AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20). Data as JSON: /api/errors/e94de3ac58d94f3a. Report an issue: GitHub.

Appendix: source

Thrown at ext/kv/01_db.ts:888

    }
    if (prefix) {
      if (start && end) {
        throw new TypeError(
          "Selector can not specify both 'start' and 'end' key when specifying 'prefix'",
        );
      }
      if (start) {
        this.#selector = { prefix, start };
      } else if (end) {
        this.#selector = { prefix, end };
      } else {
        this.#selector = { prefix };
      }
    } else {
      if (start && end) {
        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");
    }

View on GitHub (pinned to 9ad36f7a2c)