koala73/worldmonitor · error · ConvexError

digestHour must be an integer 0–23

Error message

digestHour must be an integer 0–23

What it means

Thrown by setDigestSettings when args.digestHour is present but outside [0,23], or is not an integer. digestHour represents the hour-of-day at which a digest is delivered and must be a whole number 0–23. The validation runs after the auth/entitlement gate and before any DB read.

Source

Thrown at convex/alertRules.ts:287

    }
  },
});

export const setDigestSettings = mutation({
  args: {
    variant: v.string(),
    digestMode: digestModeValidator,
    digestHour: v.optional(v.number()),
    digestTimezone: v.optional(v.string()),
  },
  handler: async (ctx, args) => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) throw new ConvexError("UNAUTHENTICATED");
    const userId = identity.subject;
    await assertProEntitlement(ctx, userId);

    if (args.digestHour !== undefined && (args.digestHour < 0 || args.digestHour > 23 || !Number.isInteger(args.digestHour))) {
      throw new ConvexError("digestHour must be an integer 0–23");
    }
    if (args.digestTimezone !== undefined) {
      try {
        Intl.DateTimeFormat(undefined, { timeZone: args.digestTimezone });
      } catch {
        throw new ConvexError("digestTimezone must be a valid IANA timezone (e.g. America/New_York)");
      }
    }

    const existing = await ctx.db
      .query("alertRules")
      .withIndex("by_user_variant", (q) =>
        q.eq("userId", userId).eq("variant", args.variant),
      )
      .unique();

    const pair = resolveEffectivePair({
      incomingDigestMode: args.digestMode,

View on GitHub (pinned to ffec79ac33)

Solutions

  1. Coerce and range-check digestHour to an integer in [0,23] before submitting (use Math.floor and clamp/mod).
  2. Ensure the UI time picker emits 0-based hours (00:00–23:59).
  3. Omit digestHour entirely if you do not need to change it — the field is optional.

Example fix

// before
await setDigestSettings({ variant, digestMode: 'daily', digestHour: 24 });
// after
const hour = Math.max(0, Math.min(23, Math.floor(pickerHour)) % 24);
await setDigestSettings({ variant, digestMode: 'daily', digestHour: hour });
Defensive patterns

Strategy: validation

Validate before calling

function isValidDigestHour(h) {
  return typeof h === 'number' && Number.isInteger(h) && h >= 0 && h <= 23;
}
if (args.digestHour !== undefined && !isValidDigestHour(args.digestHour)) {
  // surface error, do not call mutation
}

Type guard

function isDigestHour(n): n is number {
  return typeof n === 'number' && Number.isInteger(n) && n >= 0 && n <= 23;
}

Try / catch

try {
  await setDigestSettings(args);
} catch (e) {
  if (e instanceof ConvexError && /digestHour must be an integer/.test(String(e.message))) {
    // coerce: args.digestHour = Math.floor(args.digestHour) % 24 and retry once
  } else throw e;
}

Prevention

When it happens

Trigger: Calling setDigestSettings with digestHour = 24, -1, 23.5, NaN, or any non-integer numeric value.

Common situations: Client passing a 1-based hour (1–24) instead of 0-based; a time picker returning minutes or fractional hours; a timezone offset calculation producing an out-of-range value; JSON deserialization of a float where an int was expected.

Related errors


AI-assisted analysis of koala73/worldmonitor@ffec79ac33 (2026-08-12). Data as JSON: /api/errors/af4698d293a2e115. Report an issue: GitHub.