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
- Pass an integer 0–23, or omit digestHour entirely — it is optional.
- Wrap computed hours into range: ((hour % 24) + 24) % 24, and round or truncate fractional picker output.
- Map imported '24:00' values to 0 before sending.
- 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
- Build hour inputs from a 0–23 list so invalid values cannot be selected.
- Wrap timezone-adjusted hours with ((h % 24) + 24) % 24 and truncate floats.
- Map imported '24:00' to 0 before sending.
- Run the integer/range predicate client-side for inline feedback instead of a mutation round-trip.
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
- quietHoursEnd must be an integer 0–23
- quietHoursStart must be an integer 0–23
- digestTimezone must be a valid IANA timezone (e.g…
- COMPANY_MONITORING_ADMISSION_EVIDENCE_STALE
- COMPANY_MONITORING_ADMISSION_QUERY_VERSION_INVALID
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)