denoland/deno · error · NodeTypeError

ERR_INVALID_HANDLE_TYPE

ERR_INVALID_HANDLE_TYPE

Error message

This handle type cannot be sent

What it means

When a net.Socket is sent over IPC (child.send(message, socket)), the polyfill extracts a raw file descriptor the receiving process can re-open. rawFdFromTcpHandle throws ERR_INVALID_HANDLE_TYPE when the TCP handle's fdForIpc() returns a negative value — the socket exists but has no fd shareable across processes. Node applies the same rule: sockets whose descriptor cannot be duplicated are rejected.

Source

Thrown at ext/node/polyfills/internal/child_process.ts:2279

  if (!socketLists) {
    socketLists = new SafeWeakMap();
    socketListsByChild.set(child, socketLists);
  }
  let socketList = socketLists.get(server);
  if (!socketList) {
    socketList = new SocketListSend(child, server);
    socketLists.set(server, socketList);
  }
  return socketList;
}

function rawFdFromTcpHandle(tcpHandle) {
  if (typeof tcpHandle.fdForIpc !== "function") {
    notImplemented("ChildProcess.send with handle on this platform");
  }
  const rawFd = tcpHandle.fdForIpc();
  if (rawFd < 0) {
    throw new ERR_INVALID_HANDLE_TYPE();
  }
  return rawFd;
}

function getIpcHandleInfo(handle, options, target) {
  const { Socket } = lazyNet();
  const { Server: NetServer } = lazyNet();
  const { Socket: DgramSocket } = lazyDgram();
  if (ObjectPrototypeIsPrototypeOf(Socket.prototype, handle)) {
    const inner = handle._handle;
    // Match Node's handleConversion["net.Socket"].send, which returns the
    // socket's native handle. A socket without an underlying handle (e.g.
    // already destroyed) yields null; Node then strips the handle and sends
    // the message alone instead of throwing.
    if (!inner) {
      return null;
    }
    const isTcp = ObjectPrototypeIsPrototypeOf(TCP.prototype, inner);

View on GitHub (pinned to 9ad36f7a2c)

Solutions

  1. Ensure the socket is fully connected and not destroyed before send()
  2. Do not send TLS/HTTPS sockets; send the underlying plain net.Socket or connection metadata
  3. On failure, fall back to sending { host, port } and let the child reconnect itself
  4. Wrap send() in try-catch keyed on err.code === "ERR_INVALID_HANDLE_TYPE" and degrade gracefully

Example fix

// before
const sock = tls.connect(443, host);
sock.on("secureConnect", () => child.send("conn", sock)); // TLS socket not shareable

// after
const sock = net.connect(80, host);
sock.on("connect", () => {
  try {
    child.send("conn", sock);
  } catch (err) {
    if (err.code === "ERR_INVALID_HANDLE_TYPE") {
      child.send("conn-info", { host, port: 80 });
    } else throw err;
  }
});
Defensive patterns

Strategy: try-catch

Validate before calling

if (!(socket instanceof net.Socket) || socket.destroyed || socket.pending) {
  throw new Error("socket is not in a shareable state for IPC");
}

Type guard

const isShareableTcpSocket = (s) => s instanceof net.Socket && !s.destroyed && !s.pending;

Try / catch

try {
  child.send(msg, socket);
} catch (err) {
  if (err.code === "ERR_INVALID_HANDLE_TYPE") {
    child.send(msg, { host, port }); // fall back to reconnect-style handoff
  } else {
    throw err;
  }
}

Prevention

When it happens

Trigger: child.send("go", socket) where socket is a net.Socket whose fdForIpc() returns < 0 (closed, not yet connected, or otherwise unshareable); forwarding handles between cluster workers in the wrong lifecycle state.

Common situations: Sending TLS-wrapped or already-destroyed sockets; custom IPC proxies that forward handles before the connection settles; platforms where fd sharing is limited (the adjacent notImplemented path fires where fdForIpc is unavailable).

Related errors


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