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
- Validate typeof userId === 'string' && userId.length > 0 before invoking the DO; fail the job with a clear producer-side error.
- Fix the upstream producer to always stamp user_id into projection job payloads.
- If userId comes from auth, check the token/session resolution succeeded before enqueueing projection work.
- 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
- Validate job payloads at the queue consumer boundary
- Stamp user_id at production time, assert on enqueue
- Never rely on TS types alone for wire-format data
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
- canonical content
- device_id must be 1-128 characters
- deviceId must be non-empty
- name must be 1-80 characters
- ops[ ]
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)