#14755·meteor

Uncaught EPIPE in inter-process-messaging.js crashes app during rapid successive rebuilds

Author: anahar1Created Sep 14, 2026Updated Sep 15, 2026
Labelsmodern-build-stack

Meteor version

  • Meteor 3.5.2
  • Also reproducible on Meteor 3.5.1

Last known working version

Unknown.

Operating system

  • Linux / WSL2
  • Also reproduced by teammates on macOS

Description

Saving a server-affecting file several times in rapid succession can crash the entire meteor run process with an uncaught EPIPE exception.

This reproduces on a freshly created Meteor application and doesn't appear to depend on any application-specific code.

The crash happens when a build/runner child process is killed to make way for a new one while its inter-process-messaging readiness handshake is still in progress.

Reproduction

Minimal reproduction repository: https://github.com/anahar1/simple-meteor-app

git clone https://github.com/anahar1/simple-meteor-app
cd simple-meteor-app
meteor npm install
meteor run --port 3000

Once the app is running, rapidly modify a server file several times, e.g.:

for i in $(seq 1 8); do
  echo "// edit $i" >> server/main.js
  sleep 0.25
done

meteor run crashes within one or two rebuilds.

This also reproduces on a completely stock app:

meteor create test-app
cd test-app
meteor npm install
meteor run --port 3000

...then run the same modification loop.

Expected behavior

Rapid successive file changes should trigger rebuilds/restarts as usual. Even if a child process is killed while an IPC operation is pending, meteor run should handle the closed pipe gracefully rather than crashing outright.

Actual behavior

The entire meteor run process terminates with an uncaught EPIPE:

node:internal/child_process:900
        const ex = new ErrnoException(err, 'write');
                   ^

Error: write EPIPE
    at ChildProcess.target._send (node:internal/child_process:900:20)
    at ChildProcess.target.send (node:internal/child_process:775:19)
    at Timeout.poll [as _onTimeout] (packages/inter-process-messaging.js:203:26)
    at listOnTimeout (node:internal/timers:605:17)
    at processTimers (node:internal/timers:541:7) {
  errno: -32,
  code: 'EPIPE',
  syscall: 'write'
}

Node.js v24.15.0

This kills the whole dev server, not just the app — the proxy and HMR server go down with it.

Likely cause

The crash originates in makeReadyPromise() in packages/inter-process-messaging/inter-process-messaging.js. Its poll() function pings a freshly spawned child process on a backoff until it becomes ready:

function poll() {
  if (readyResolvers.has(pingMessage.id)) {
    otherProcess.send(pingMessage, error => {
      if (error) {
        reject(error);
      } else {
        setTimeout(poll, delay_ms);
        delay_ms *= backoff_factor;
      }
    });
  }
}

otherProcess.send() is expected to report failures through that callback — there's even a gracefulErrorHandler elsewhere in the same file that explicitly ignores EPIPE when it arrives that way. But when the child's IPC pipe has already been torn down (as happens when a previous build/runner process is killed to spawn a new one before its first PING/PONG handshake completes), Node's ChildProcess.send() can throw synchronously instead of invoking the callback with an error. Because this call sits inside a bare setTimeout with no surrounding try/catch, that synchronous throw becomes an uncaught exception that takes down the whole process, not just the child.

A minimal fix would be to wrap the send() call in poll() and route a synchronous throw through the same rejection path already used for the async-callback error case:

function poll() {
  if (readyResolvers.has(pingMessage.id)) {
    try {
      otherProcess.send(pingMessage, error => {
        if (error) {
          reject(error);
        } else {
          setTimeout(poll, delay_ms);
          delay_ms *= backoff_factor;
        }
      });
    } catch (error) {
      reject(error);
    }
  }
}