Skip to content

fix(desktop): async EPIPE on process.stderr bypasses console transport try-catch #37749

Description

@Moonafrost

Description

OpenCode Desktop v1.18.3 still crashes with EPIPE (Error: EPIPE: broken pipe, write) in the main process, despite the fix in PR #25980.

The existing fix wraps log.transports.console.writeFn in a try-catch that catches synchronous EPIPE (thrown directly by console.error()). However, EPIPE can also arrive asynchronously via the process.stderr 'error' event, which is outside the call stack and cannot be caught by try-catch.

Stack trace

Error: EPIPE: broken pipe, write
  at Socket.write (node:internal/net:75:18)
  at writeOrBuffer (node:internal/streams/writable:570:12)
  at write (node:internal/streams/writable:499:10)
  at Writable.write (node:internal/streams/writable:508:10)
  at console.value (node:internal/console/constructor:313:16)
  at console.error (node:internal/console/constructor:444:26)
  at transport.writeFn
  at log.transports.console.writeFn
  at transport
  at Logger.processMessage

Root cause

The current initConsoleTransport() in out/main/index.js only guards the synchronous path:

function initConsoleTransport() {
  const write2 = log.transports.console.writeFn.bind(log.transports.console);
  log.transports.console.writeFn = (options) => {
    try {
      write2(options);
    } catch (err) {
      if (!isBrokenPipe(err)) throw err;
      log.transports.console.level = false;
    }
  };
}

But process.stderr (a net.Socket) can emit an asynchronous 'error' event when the write fails. This event is not in the call stack of writeFn, so the try-catch does not intercept it. With no process.on('uncaughtException') handler registered, this becomes an uncaught exception that crashes the main process.

Additionally, electron-log's processInternalErrorFn (createDefaultLogger.js:35-42) calls writeFn directly when a transport fails. If the error is async, this can cause recursive calls.

Reproduction

  1. Launch OpenCode Desktop v1.18.3 on Windows via the desktop shortcut
  2. Use the app normally -- the crash typically occurs when the logger writes during agent responses
  3. Observe the "Uncaught Exception" dialog

The exact timing depends on when the parent process closes its end of the stdout/stderr pipe.

Suggested fix

Add an async EPIPE handler on process.stderr in initConsoleTransport():

function initConsoleTransport() {
  const write2 = log.transports.console.writeFn.bind(log.transports.console);
  log.transports.console.writeFn = (options) => {
    try {
      write2(options);
    } catch (err) {
      if (!isBrokenPipe(err)) throw err;
      log.transports.console.level = false;
    }
  };

  // Guard against async EPIPE from process.stderr 'error' event
  const onStderrError = (err) => {
    if (isBrokenPipe(err)) {
      log.transports.console.level = false;
      process.stderr.removeListener('error', onStderrError);
    }
  };
  process.stderr.on('error', onStderrError);
}

Alternatively, add a scoped process.on('uncaughtException') at the main process entry point that filters EPIPE errors specifically:

process.on('uncaughtException', (error) => {
  if (error && (error.code === 'EPIPE' || (error.message && error.message.includes('EPIPE')))) {
    log.transports.console.level = false;
    return;
  }
  console.error('Uncaught Exception:', error);
  app.quit();
});

Environment

  • OS: Windows 10/11
  • OpenCode Desktop: 1.18.3
  • Launched via: Windows shortcut (.lnk)

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions