thedotmack/claude-mem · error · Error

projection_error: user_id must be non-empty

Error message

projection_error: user_id must be non-empty

What it means

getProjectionPage() validates that userId is a non-empty string before paging canonical ops for that user's projection. An empty or non-string userId cannot identify any projection target, so the DO throws this projectionError immediately after the lease check.

Solutions

  1. Validate typeof userId === 'string' && userId.length > 0 before invoking the DO; fail the job with a clear producer-side error.
  2. Fix the upstream producer to always stamp user_id into projection job payloads.
  3. If userId comes from auth, check the token/session resolution succeeded before enqueueing projection work.
  4. At runtime, coerce with a guard: if (!userId) skip/reject the job rather than calling the hub.

Example fix

// before
await hub.getProjectionPage(lease.lease_token, target, job.userId);
// after
if (typeof job.userId !== 'string' || job.userId.length === 0) {
  throw new Error(`projection job ${job.id} missing userId`);
}
await hub.getProjectionPage(lease.lease_token, target, job.userId);
Defensive patterns

Strategy: validation

Validate before calling

if (typeof userId !== 'string' || userId.length === 0) {
  throw new TypeError('projection job requires non-empty userId');
}

Type guard

function hasUserId(job: unknown): job is { userId: string } & Record<string, unknown> {
  return typeof job === 'object' && job !== null &&
    typeof (job as any).userId === 'string' && (job as any).userId.length > 0;
}

Try / catch

try {
  page = hub.getProjectionPage(token, target, userId);
} catch (e) {
  if (String(e).includes('user_id must be non-empty')) {
    deadLetter(job, 'missing userId'); return; // don't retry producer bugs
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling getProjectionPage(token, targetSeq, '') or passing undefined/null/a non-string as userId — e.g. userId omitted from a job payload, or a decode step producing an empty string for a missing field.

Common situations: Projection worker consuming queue messages where the user_id attribute was never set; JSON payloads with "userId": "" from an upstream producer bug; TS types claiming string while runtime data is empty after a schema change.

Understand the failure class

Background: "must not be empty", "cannot be empty" — required-field validation errors across open-source libraries — this error's family across 41 libraries.

Related errors


AI-assisted analysis of thedotmack/claude-mem@d8bc9755e7 (2026-09-17). Data as JSON: /api/errors/c78e01f482b1f647. Report an issue: GitHub.

Appendix: source

Thrown at workers/sync-hub/src/do/SyncHub.ts:677

			acquired: true,
			lease_token: token,
			epoch: this.meta("epoch"),
			head_seq: head,
			projected_seq: projected,
			target_seq: target,
		};
	}

	getProjectionPage(
		leaseToken: string,
		targetSeq: string,
		userId: string,
		maxOps = PROJECTION_PAGE_MAX_OPS,
		maxBytes = PROJECTION_PAGE_MAX_BYTES,
		now = Date.now(),
	): ProjectionPage {
		this.assertLease(leaseToken, now);
		if (typeof userId !== "string" || userId.length === 0) throw projectionError("user_id must be non-empty");
		const target = assertCanonicalDecimal(targetSeq);
		const projected = this.projectedSeq();
		const epoch = this.meta("epoch");
		if (compareCanonicalDecimals(target, this.headSeq()) > 0) throw projectionError("target_seq exceeds head_seq");
		const limit = Math.min(PROJECTION_PAGE_MAX_OPS, Math.max(1, Math.floor(maxOps)));
		const byteLimit = Math.min(PROJECTION_PAGE_MAX_BYTES, Math.max(1, Math.floor(maxBytes)));
		const rows = this.ctx.storage.sql.exec<{
			seq: string;
			body: string;
			operation_sha256: string;
			server_ts: string;
		}>(
			`SELECT seq, body, operation_sha256, server_ts
			 FROM canonical_ops
			 WHERE (LENGTH(seq) > LENGTH(?) OR (LENGTH(seq) = LENGTH(?) AND seq > ?))
			   AND (LENGTH(seq) < LENGTH(?) OR (LENGTH(seq) = LENGTH(?) AND seq <= ?))
			 ORDER BY LENGTH(seq), seq LIMIT ?`,
			projected,

View on GitHub (pinned to d8bc9755e7)