denoland/deno · error · RangeError

ERR_INVALID_FD

ERR_INVALID_FD

Error message

"fd" must be a positive integer: ${fd}

What it means

The legacy tty.ReadStream constructor validates fd before anything else: fd must satisfy `fd >> 0 === fd && fd >= 0`, i.e. a non-negative int32 integer. Negative numbers, fractions, NaN, strings, BigInts, or values above 2147483647 throw ERR_INVALID_FD; fd 0 (stdin) is accepted despite the 'positive integer' message wording. After this check, non-stdio fds additionally require --allow-all permission in Deno.

Source

Thrown at ext/node/polyfills/tty_esm.ts:31

} = primordials;
const {
  ERR_INVALID_FD,
  ERR_TTY_INIT_FAILED,
} = core.loadExtScript("ext:deno_node/internal/errors.ts");

const { isatty } = core.loadExtScript("ext:deno_node/tty.js");

// ReadStream needs to be callable without `new` to match Node.js behavior.
// We use a wrapper function that delegates to the actual class.
// deno-lint-ignore no-explicit-any
function ReadStream(this: any, fd: number, options?: unknown) {
  if (!ObjectPrototypeIsPrototypeOf(ReadStream.prototype, this)) {
    // deno-lint-ignore no-explicit-any
    return new (ReadStream as any)(fd, options);
  }

  if (fd >> 0 !== fd || fd < 0) {
    throw new ERR_INVALID_FD(fd);
  }

  // Non-stdio fds require --allow-all
  op_tty_check_fd_permission(fd);

  // deno-lint-ignore no-explicit-any
  const ctx: any = {};
  const tty = new TTY(fd, ctx);
  if (ctx.code !== undefined) {
    throw new ERR_TTY_INIT_FAILED(ctx);
  }
  FunctionPrototypeCall(Socket, this, {
    readableHighWaterMark: 0,
    handle: tty,
    manualStart: true,
    // deno-lint-ignore no-explicit-any
    ...(options as any),
  });

View on GitHub (pinned to 9ad36f7a2c)

Solutions

  1. Convert with Number() and validate: `Number.isInteger(fd) && fd >= 0 && fd <= 0x7fffffff`
  2. Use process.stdin/stdout/stderr or the literal fds 0, 1, 2 instead of raw numbers
  3. Grant --allow-all when wrapping fds beyond stdio in Deno (op_tty_check_fd_permission)
  4. Prefer tty.isatty(fd) checks plus high-level stream APIs over constructing ReadStream directly

Example fix

// before
const fd = process.env.FD; // string from env
new tty.ReadStream(fd); // ERR_INVALID_FD

// after
const fd = Number(process.env.FD);
if (!Number.isInteger(fd) || fd < 0 || fd > 0x7fffffff) {
  throw new RangeError(`bad fd: ${process.env.FD}`);
}
new tty.ReadStream(fd);
Defensive patterns

Strategy: type-guard

Validate before calling

const fd = Number(rawFd);
if (!Number.isInteger(fd) || fd < 0 || fd > 0x7fffffff) {
  throw new Error(`fd must be a non-negative int32, got ${String(rawFd)}`);
}
const rs = new tty.ReadStream(fd);

Type guard

const isValidFd = (fd: unknown): fd is number =>
  typeof fd === "number" &&
  Number.isInteger(fd) &&
  fd >= 0 &&
  fd <= 0x7fffffff;

Try / catch

try {
  stream = new tty.ReadStream(fd);
} catch (e: any) {
  if (e?.code === "ERR_INVALID_FD") {
    throw new Error(`invalid fd from configuration: ${String(fd)}`);
  }
  throw e;
}

Prevention

When it happens

Trigger: `new tty.ReadStream(process.argv[2])` with an unconverted string; a fractional fd like 1.5; a negative sentinel like -1; `parseInt(x)` returning NaN; fds above 2^31-1 obtained from another API.

Common situations: Reading fd numbers from environment variables or CLI args without Number(); fd variables that are undefined after a failed lookup; code ported from runtimes with different fd validation rules.

Related errors


AI-assisted analysis of denoland/deno@9ad36f7a2c (2026-08-20). Data as JSON: /api/errors/7064ef2cba439f6b. Report an issue: GitHub.