hcengineering/platform · error

Interval must be a finite number >= 1 (seconds)

Error message

Interval must be a finite number >= 1 (seconds)

What it means

register schedules a TickHandler to fire every `interval` seconds, so the interval must be a finite number >= 1. NaN, Infinity, 0, negatives, and fractional sub-second intervals are rejected at registration time. It returns an unregister function only when validation passes.

Source

Thrown at foundations/net/packages/core/src/utils.ts:48

  tickListeners = new Map<number, (() => void)[]>()

  started: boolean = false

  constructor (readonly tps: number) {
    if (tps > 1000 || tps < 1) {
      throw new Error('Ticks per second has an invalid value: must be >= 1 && <= 1000')
    }
  }

  now (): number {
    // Use performance.now() when available, otherwise fall back to Date.now()
    // performance is available in recent Node versions, but guard for portability.
    return (globalThis as any).performance?.now?.() ?? Date.now()
  }

  register (handler: TickHandler, interval: number): () => void {
    if (!Number.isFinite(interval) || interval < 1) {
      throw new Error('Interval must be a finite number >= 1 (seconds)')
    }
    const handlerId = this.hashCounter++
    this.handlers.set(handlerId, [handler, handlerId % this.tps, interval])
    return () => {
      this.handlers.delete(handlerId)
    }
  }

  async tick (): Promise<void> {
    this._tick++

    // Handle tick listeners
    if (this.tickListeners.size > 0) {
      const listeners = this.tickListeners.get(this._tick) ?? []
      this.tickListeners.delete(this._tick)
      for (const listener of listeners) {
        try {
          listener()

View on GitHub (pinned to 63e28dc964)

Solutions

  1. Pass a whole number of seconds >= 1 (e.g. register(handler, 5)).
  2. Convert from milliseconds explicitly: Math.ceil(ms / 1000), and guard against < 1.
  3. Sanitize the source value: Number.isFinite(v) ? v : fallback before calling register.
  4. If you need sub-second granularity, use setInterval directly or raise tps instead of passing fractional intervals.

Example fix

// before
ticker.register(handler, Number(process.env.INTERVAL_MS)) // NaN or ms-as-seconds
// after
const secs = Math.max(1, Math.ceil(Number(process.env.INTERVAL_MS ?? 1000) / 1000))
ticker.register(handler, secs)
Defensive patterns

Strategy: validation

Validate before calling

function toIntervalSeconds(raw: unknown, fallback = 5): number {
  const n = typeof raw === 'number' ? raw : Number(raw)
  if (!Number.isFinite(n)) return fallback
  return Math.max(1, Math.ceil(n)) // supports ms callers via explicit conversion upstream
}
ticker.register(handler, toIntervalSeconds(process.env.HANDLER_INTERVAL_S))

Type guard

function isValidInterval(v: unknown): v is number {
  return typeof v === 'number' && Number.isFinite(v) && v >= 1
}

Try / catch

try {
  const cancel = ticker.register(handler, interval)
  registered.push(cancel)
} catch (e) {
  if ((e as Error).message.includes('Interval must be')) {
    console.warn(`invalid interval ${interval}, using 5s`)
    registered.push(ticker.register(handler, 5))
  } else throw e
}

Prevention

When it happens

Trigger: Calling ticker.register(handler, interval) with interval = 0, a negative number, NaN (e.g. from parseInt of a bad env var), or Infinity — all of which fail the Number.isFinite/interval < 1 check.

Common situations: Env/config strings like '' or 'abc' converting to NaN; mistaking milliseconds for seconds (500 intended as 0.5s); uninitialized variable defaulting to undefined→NaN; unit confusion after migrating from a ms-based timer API.

Related errors


AI-assisted analysis of hcengineering/platform@63e28dc964 (2026-08-29). Data as JSON: /api/errors/a5e23f1786a0b2b4. Report an issue: GitHub.