koala73/worldmonitor · error · ConvexError
userId is required
Error message
userId is required
What it means
The internal checkout mutation requires an explicit userId argument because it runs without a browser identity (internal functions have no auth context). An empty/missing args.userId makes the target user unattributable, so the mutation throws before validating the product.
Solutions
- Resolve and pass the Convex user's _id as userId before calling the mutation.
- Fix the upstream lookup (email -> user mapping) that produced an undefined userId.
- Skip checkout creation for requests with no attributable user instead of calling the mutation with a blank id.
Example fix
// before
await ctx.runMutation(api.payments.checkout.internalCreateCheckout, { productId } as any);
// after
if (!user?._id) throw new Error("cannot checkout without user");
await ctx.runMutation(api.payments.checkout.internalCreateCheckout, { userId: user._id, productId }); Defensive patterns
Strategy: validation
Validate before calling
if (!userId) throw new Error("internalCreateCheckout requires a resolved userId");
await runMutation(api.payments.checkout.internalCreateCheckout, { userId, productId }); Type guard
function hasUserId(u: { _id?: string } | null | undefined): u is { _id: string } {
return typeof u?._id === "string" && u._id.length > 0;
} Try / catch
try {
await internalCreateCheckout({ userId, productId });
} catch (e) {
if (String(e?.message) === "userId is required") {
logUnattributableCheckoutRequest(context); // do not retry without a user
}
} Prevention
- Type internal callers against a required userId parameter so TypeScript rejects omitted fields.
- Resolve user identity upstream and fail fast when lookup yields nothing.
- Never call internal checkout mutations from paths lacking a verified user reference (e.g. anonymous webhook events).
When it happens
Trigger: Calling the internal checkout mutation (the one with bypassPendingGuard in its args at checkout.ts:494) with userId undefined, null, or an empty string — e.g. from a webhook/cron where the user lookup failed.
Common situations: Webhook payload lacked the user reference used to resolve userId; caller code passes a variable that is undefined after a failed users-table lookup; cron job scheduling checkouts without resolving a user; omitting the field entirely from the function args object.
Understand the failure class
Background: "missing required argument" and "the following required arguments were not provided": what required-argument errors mean and how to fix them — this error's family across 20 libraries.
Related errors
- COMPANY_MONITORING_${field}_INVALID
- COMPANY_MONITORING_MODEL_VERSION_INVALID
- COMPANY_MONITORING_EVIDENCE_REVISION_INVALID
- INCOMPATIBLE_DELIVERY
- COUNTRIES_LIMIT_EXCEEDED
AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15).
Data as JSON: /api/errors/4931f109c08e1e60.
Report an issue: GitHub.
Appendix: source
Thrown at convex/payments/checkout.ts:494
// Internal action: called by /relay/create-checkout with trusted userId
// ---------------------------------------------------------------------------
export const internalCreateCheckout = internalAction({
args: {
userId: v.string(),
email: v.optional(v.string()),
name: v.optional(v.string()),
productId: v.string(),
returnUrl: v.optional(v.string()),
discountCode: v.optional(v.string()),
referralCode: v.optional(v.string()),
attributionSource: v.optional(v.string()),
// See createCheckout — skips only the pending-payment guard (#4438).
bypassPendingGuard: v.optional(v.boolean()),
},
handler: async (ctx, args) => {
if (!args.userId) {
throw new ConvexError("userId is required");
}
requireCheckoutProduct(args.productId);
if (args.bypassPendingGuard) {
// See createCheckout — audit the pending-guard bypass (#4438 review).
console.info(`[checkout] pending-payment guard bypassed user=${args.userId} product=${args.productId}`);
}
// Both guards concurrently (no shared data); subscription block still wins,
// bypass skips the pending query (#4438 review).
const [blocking, pending] = await Promise.all([
getCheckoutBlockingSubscription(ctx, args.userId, args.productId),
args.bypassPendingGuard
? Promise.resolve(null)
: getCheckoutBlockingPendingPayment(ctx, args.userId, args.productId),
]);
if (blocking) {
return buildBlockedCheckoutResponse(blocking);
}
if (pending) {View on GitHub (pinned to 7d06c8633d)