denoland/deno · error · TypeError

expireIn must be a non-negative integer: received ${expireIn

Error message

expireIn must be a non-negative integer: received ${expireIn}

What it means

Deno.Kv expireIn (used by kv.set/get with { expireIn } and in atomic checks/mutations) must be a non-negative integer number of milliseconds. The guard rejects NaN, Infinity, fractional values, and negatives because a non-finite or fractional value would otherwise reach the native layer and overflow/panic while computing the absolute expiry timestamp.

Source

Thrown at ext/kv/01_db.ts:86

    throw new TypeError(`Delay must be >= 0: received ${delay}`);
  }
  if (delay > maxQueueDelay) {
    throw new TypeError(
      `Delay cannot be greater than 30 days: received ${delay}`,
    );
  }
  if (NumberIsNaN(delay)) {
    throw new TypeError("Delay cannot be NaN");
  }
}

function validateExpireIn(expireIn: number | undefined) {
  if (expireIn === undefined) return;
  // Reject NaN, Infinity, fractional and negative values. A non-finite
  // expireIn otherwise reaches the native layer and overflows when computing
  // the absolute expiry, panicking the process.
  if (!NumberIsInteger(expireIn) || expireIn < 0) {
    throw new TypeError(
      `expireIn must be a non-negative integer: received ${expireIn}`,
    );
  }
}

const maxQueueBackoffIntervals = 5;
const maxQueueBackoffInterval = 60 * 60 * 1000;

function validateBackoffSchedule(backoffSchedule: number[]) {
  if (backoffSchedule.length > maxQueueBackoffIntervals) {
    throw new TypeError(
      `Invalid backoffSchedule, max ${maxQueueBackoffIntervals} intervals allowed`,
    );
  }
  for (let i = 0; i < backoffSchedule.length; ++i) {
    const interval = backoffSchedule[i];
    if (
      interval < 0 || interval > maxQueueBackoffInterval ||

View on GitHub (pinned to 89f33cbef2)

Solutions

  1. Always compute expireIn as an integer ms count: const ttlMs = Math.floor(ttlSeconds * 1000).
  2. Validate user/config input: Number.isInteger(n) && n >= 0 before calling set.
  3. Omit expireIn entirely (undefined is allowed) for no expiry.
  4. Convert string config values with Number() and validate finiteness.

Example fix

// before
await kv.set(["session", id], data, { expireIn: ttlSeconds }); // e.g. 3600.5 or seconds-as-ms

// after
const expireIn = Math.floor(ttlSeconds * 1000);
await kv.set(["session", id], data, { expireIn });
Defensive patterns

Strategy: validation

Validate before calling

function toExpireIn(ms: unknown): number | undefined {
  if (ms === undefined || ms === null) return undefined;
  const n = Math.floor(Number(ms));
  if (!Number.isFinite(n) || n < 0) throw new Error(`invalid expireIn: ${ms}`);
  return n;
}
await kv.set(key, value, { expireIn: toExpireIn(ttlMs) });

Type guard

function isValidExpireIn(v: unknown): v is number { return typeof v === "number" && Number.isInteger(v) && v >= 0; }

Prevention

When it happens

Trigger: kv.set(key, val, { expireIn: 1.5 }) fractional; { expireIn: -1000 }; { expireIn: NaN } from arithmetic; { expireIn: Infinity } or { expireIn: 24 * 60 * 60 * 1000 * 365 } overflowing past safe integers is fine numerically but Infinity from division by 0 is not; expireIn: '3600' as a string coerces to NaN under NumberIsInteger.

Common situations: TTL configs in seconds passed directly as ms (or vice versa producing fractions after division); expirations computed from user input; JSON strings not converted to numbers; clock arithmetic with undefined; porting Redis TTL semantics (seconds) to Deno KV (milliseconds).

Related errors


AI-assisted analysis of denoland/deno@89f33cbef2 (2026-08-16). Data as JSON: /api/errors/e9123eb251c2b6d4. Report an issue: GitHub.