koala73/worldmonitor · error · ConvexError

quietHoursStart must be an integer 0–23

Error message

quietHoursStart must be an integer 0–23

What it means

validateQuietHoursArgs (convex/alertRules.ts:419) runs for both setQuietHours and setQuietHoursForUser. When quietHoursStart is supplied it must be an integer in 0–23 (Number.isInteger plus bounds check); otherwise the mutation throws ConvexError 'quietHoursStart must be an integer 0–23' (string data, no code). The arg is v.optional(v.number()), so non-numeric values fail Convex's own validator first.

Solutions

  1. Pass an integer 0–23 or omit quietHoursStart.
  2. Wrap computed values: ((hour % 24) + 24) % 24 and Math.trunc fractional input.
  3. Build the picker options from 0–23 so out-of-range values cannot be selected.
  4. Validate client-side with the same integer/range predicate before calling the mutation.

Example fix

// before
await mutateAPI.alertRules.setQuietHours({ variant: 'default', quietHoursEnabled: true, quietHoursStart: 22.5, quietHoursEnd: 7 });
// ConvexError: quietHoursStart must be an integer 0–23

// after
const start = ((Math.trunc(pickedStart) % 24) + 24) % 24;
await mutateAPI.alertRules.setQuietHours({ variant: 'default', quietHoursEnabled: true, quietHoursStart: start, quietHoursEnd: 7 });
Defensive patterns

Strategy: validation

Validate before calling

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

if (quietHoursStart !== undefined && !isValidHour(quietHoursStart)) {
  setFieldError('quietHoursStart', 'Pick a whole hour between 0 and 23');
  return;
}
await mutateAPI.alertRules.setQuietHours({ variant, quietHoursEnabled, quietHoursStart, quietHoursEnd });

Type guard

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

Try / catch

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

Prevention

When it happens

Trigger: setQuietHours (or setQuietHoursForUser via runMutation) with quietHoursStart: 9.5 from a fractional time picker; negative values from a TZ conversion; 24 from a '24:00' convention; a computed NaN forwarded unchecked.

Common situations: Quiet-hours sliders emitting floats; overnight-window math (start 22 minus offset) going negative; imported notification settings from systems using 1–24 hours; UI selects built from '00:00'–'24:00' strings.

Related errors


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

Appendix: source

Thrown at convex/alertRules.ts:425

  },
});

const QUIET_HOURS_ARGS = {
  variant: v.string(),
  quietHoursEnabled: v.boolean(),
  quietHoursStart: v.optional(v.number()),
  quietHoursEnd: v.optional(v.number()),
  quietHoursTimezone: v.optional(v.string()),
  quietHoursOverride: v.optional(quietHoursOverrideValidator),
} as const;

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

export const setQuietHours = mutation({
  args: QUIET_HOURS_ARGS,
  handler: async (ctx, args) => {
    const identity = await ctx.auth.getUserIdentity();
    if (!identity) throw new ConvexError("UNAUTHENTICATED");

View on GitHub (pinned to eeab0a219f)