pydantic/monty · error · napi::Error

{name} must be an integer

Error message

{name} must be an integer

What it means

napi-rs `Error` (Status::InvalidArg) thrown by `js_number_to_u64` when a numeric option has a fractional part. The target `u64`/`usize` limit must be a whole number, so values like 1.5 are rejected even if positive and in range.

Source

Thrown at crates/monty-js/src/limits.rs:109

/// Converts a JavaScript `number` used for a size/count limit into `u64`.
///
/// JavaScript numbers are IEEE-754 doubles, so integers above `2^53 - 1`
/// cannot be represented exactly. Rejecting values outside the safe integer
/// range avoids silently rounding resource limits at the napi boundary.
///
/// Returns `Err` for non-finite, negative, fractional, or out-of-range inputs.
/// This helper does not panic.
pub(crate) fn js_number_to_u64(value: f64, name: &str) -> Result<u64> {
    const JS_MAX_SAFE_INTEGER: u64 = (1_u64 << 53) - 1;

    match value {
        v if !v.is_finite() => Err(Error::new(
            Status::InvalidArg,
            format!("{name} must be a finite number"),
        )),
        v if v < 0.0 => Err(Error::new(Status::InvalidArg, format!("{name} must be non-negative"))),
        v if v.fract() != 0.0 => Err(Error::new(Status::InvalidArg, format!("{name} must be an integer"))),
        v if v > JS_MAX_SAFE_INTEGER as f64 => Err(Error::new(
            Status::InvalidArg,
            format!("{name} must be a safe integer (<= {JS_MAX_SAFE_INTEGER})"),
        )),
        v => {
            #[expect(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
            let value = v as u64;
            Ok(value)
        }
    }
}

View on GitHub (pinned to adc986b362)

Solutions

  1. Use `Math.floor`/`Math.round`/`Math.trunc` to integerize the value before passing it
  2. Compute sizes with integer arithmetic where possible
  3. Guard with `Number.isInteger(value)` before the call

Example fix

// before
const maxMemory = totalMemory / workers; // e.g. 1048576.5
// after
const maxMemory = Math.floor(totalMemory / workers);
Defensive patterns

Strategy: validation

Validate before calling

function assertInteger(name, v) {
  if (!Number.isInteger(v)) throw new TypeError(`${name} must be an integer`);
  return v;
}

Type guard

const isNonNegativeInt = (v) => Number.isInteger(v) && v >= 0;

Try / catch

try {
  await Monty.create({ maxMemory: rawBytes });
} catch (e) {
  if (/must be an integer/.test(e?.message ?? '')) {
    await Monty.create({ maxMemory: Math.floor(rawBytes) });
  } else throw e;
}

Prevention

When it happens

Trigger: Passing a non-integer such as `requestTimeout: 1.5`, `maxMemory: 1048576.25`, or a value computed via division (`total / workers`) into a limit option routed through `js_number_to_usize`.

Common situations: Dividing memory or worker counts across processes without flooring; percentages converted to bytes; numbers read from config files with decimal units.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of pydantic/monty@adc986b362 (2026-09-13). Data as JSON: /api/errors/bb0801aa3bb07e62. Report an issue: GitHub.