koala73/worldmonitor · error · ConvexError

digestHour must be an integer 0–23

Error message

digestHour must be an integer 0–23

What it means

setDigestSettings validates the optional digestHour argument when supplied: it must satisfy Number.isInteger and the range 0–23 (convex/alertRules.ts:286). Fractional hours (7.5), negative values, 24+, or NaN throw a plain ConvexError with string data 'digestHour must be an integer 0–23' (no structured code). The arg itself is v.optional(v.number()), so non-numeric types are rejected earlier by the Convex validator with a different error.

Solutions

  1. Pass an integer 0–23, or omit digestHour entirely — it is optional.
  2. Wrap computed hours into range: ((hour % 24) + 24) % 24, and round or truncate fractional picker output.
  3. Map imported '24:00' values to 0 before sending.
  4. Run the same integer/range check client-side before calling (see defense) so the user gets inline feedback instead of a mutation error.

Example fix

// before
await mutateAPI.alertRules.setDigestSettings({ variant: 'default', digestMode: 'daily', digestHour: 24 });
// ConvexError: digestHour must be an integer 0–23

// after — wrap into range and force integer
const digestHour = ((Math.trunc(selectedHour) % 24) + 24) % 24;
await mutateAPI.alertRules.setDigestSettings({ variant: 'default', digestMode: 'daily', digestHour });
Defensive patterns

Strategy: validation

Validate before calling

// Same predicate the server applies, run before the mutation.
const isValidHour = (h: unknown): h is number =>
  typeof h === 'number' && Number.isInteger(h) && h >= 0 && h <= 23;

const digestHour = computedHour === undefined ? undefined : ((Math.trunc(computedHour) % 24) + 24) % 24;
if (digestHour !== undefined && !isValidHour(digestHour)) {
  throw new RangeError('digestHour must be an integer 0-23');
}
await mutateAPI.alertRules.setDigestSettings({ variant, digestMode, digestHour });

Type guard

const isValidDigestHour = (h: unknown): h is number =>
  typeof h === 'number' && Number.isInteger(h) && h >= 0 && h <= 23;

Try / catch

try {
  await mutateAPI.alertRules.setDigestSettings(args);
} catch (err) {
  if (err instanceof ConvexError && err.data === 'digestHour must be an integer 0–23') {
    setFieldError('digestHour', 'Pick a whole hour between 0 and 23');
    return;
  }
  throw err;
}

Prevention

When it happens

Trigger: setDigestSettings with digestHour: 7.5 (a time picker emitting fractional hours); digestHour: 24 (a '24:00' convention); a computed hour like utcHour + tzOffset overflowing past 23 without wrapping; parseFloat/parseInt producing NaN or a float from free-text input.

Common situations: Digest-time pickers built from timezone conversions that can exceed the range (e.g. UTC 22 + offset 3 = 25); importing cron-style hours where 24 means midnight; UI sliders emitting floats; copy-paste of '8:30' parsed as 8.5.

Related errors


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

Appendix: 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 eeab0a219f)