Skip to content

EmulatorLog.flush() crashes with TypeError when process.send() reports an error, destroying the original error #10876

Description

@mattsears18

[REQUIRED] Environment info

firebase-tools: 15.24.0 (also present on master today)

Platform: macOS (platform-independent — pure JS logic bug)

[REQUIRED] Test case

The defect is in src/emulator/types.ts, in EmulatorLog.flush():

EmulatorLog.WAITING_FOR_FLUSH = true;
if (process.send) {
  (process.send as any)(nextMsg, undefined, {}, (err: any) => {
    if (err) {
      process.stderr.write(err);   // <-- err is an Error object
    }

    EmulatorLog.WAITING_FOR_FLUSH = EmulatorLog.LOG_BUFFER.length > 0;
    this.flush();
  });
}

process.send()'s callback receives an Error object (per the Node docs), but stream.write() accepts only a string, Buffer, TypedArray, or DataView. Passing an Error makes write() throw synchronously.

Minimal reproduction of the underlying write() behavior:

process.stderr.write(new Error('boom'));
// TypeError [ERR_INVALID_ARG_TYPE]: The "chunk" argument must be of type string
// or an instance of Buffer, TypedArray, or DataView. Received an instance of Error

[REQUIRED] Steps to reproduce

The code path fires whenever the functions-runtime child process fails to IPC a log message back to the CLI — i.e. when something has already gone wrong with that subprocess (it died, was killed, or the channel closed). We hit it on CI runners under memory/CPU saturation, where the emulator subprocess is killed mid-run.

It is not reproducible on demand without inducing an IPC failure, because it requires process.send()'s callback to be invoked with a non-null err.

[REQUIRED] Expected behavior

The error's message/stack is written to stderr, so the operator can see why the IPC to the functions runtime failed.

[REQUIRED] Actual behavior

process.stderr.write(err) throws TypeError: The "chunk" argument must be of type string or an instance of Buffer, TypedArray, or DataView. Received an instance of Error, inside a callback on the microtask queue with no surrounding try/catch. That makes it an uncaught exception, so:

  1. The original error is destroyed. Whatever err actually said — the real reason the emulator subprocess became unreachable — is never printed.
  2. The replacement error is misleading. Operators see a TypeError about stream chunk types, which points at Node's stream internals rather than at the emulator failure that actually happened.

Observed stack:

TypeError: The "chunk" argument must be of type string or an instance of Buffer, TypedArray, or DataView. Received an instance of Error
  at _write (node:internal/streams/writable:480)
  at Writable.write (node:internal/streams/writable:508)
  at node_modules/firebase-tools/lib/emulator/types.js:180
      process.stderr.write(err);
  at process.processTicksAndRejections (node:internal/process/task_queues:89)

The practical cost is diagnostic and it compounds: this fires precisely when an emulator subprocess has failed, which is exactly the moment the error text matters most — and firebase-tools' own reporter is what swallows it. It has made two separate emulator-flake investigations in our repo materially harder, because CI logs contain only the TypeError and never the underlying cause.

Suggested fix

Stringify before writing:

(process.send as any)(nextMsg, undefined, {}, (err: any) => {
  if (err) {
    process.stderr.write(`${(err && (err.stack || err.message)) || err}\n`);
  }
  ...
});

Happy to open a PR if that shape is agreeable.

For reference, we are carrying this locally as a patch-package patch against 15.24.0 until it is fixed upstream.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions