Note: I already found a solution to this problem, posting it here for posterity. See the selected answer.


The following (simplified) code throws an uncatchable "write EPIPE" (and in some scenarios "write EOF") error:

const { exec } = require("child_process");

const veryLargeString = "x".repeat(10 * 1024 * 1024);

const p = exec("gibberishThatWillFailImmediately");

p.stdin.write(veryLargeString);

My failed attempts at the problem:

  • Checking the stdin.destroyed flag before writing
  • Checking the stdin.writeableEnded flag before writing
  • Chunking the input and checking stdin.writeableEnded before each chunk. This one lead to undeterministic behavior.
  • Wrapping the stdin.write(data) line with try-catch
  • Calling stdin.end(data) instead of stdin.write(data)
  • Pass stdin.write() a callback that should get any error that occurs. The callback got the error, but didn't prevent it from being thrown.
1

There are 1 answers

0
Yarin On BEST ANSWER

Registering an 'error' handler to the stdin stream seems to prevent the error from being thrown. Like this:

const { exec } = require("child_process");

const veryLargeString = "x".repeat(10 * 1024 * 1024);

const p = exec("gibberishThatWillFailImmediately");

p.stdin.on('error', (error) => console.log("error caught: ", error));

p.stdin.write(veryLargeString);

Here's an example that returns a promise containing the error or null if no error occured:

const { exec } = require("child_process");

const veryLargeString = "x".repeat(10 * 1024 * 1024);

function safelyWriteDataToStdin(stdin, data) {
  // Register an awaitable callback that will capture any error occuring during the write operation
  const promise = new Promise((resolve, _reject) => {
    // Using once() and not on() to remove the listener after the first catch.
    stdin.once("error", (error) => resolve(error));

    // stdin.end(data, callback) can probably be used here, but I keep the `write()` just in case `end()`'s callback is called before the 'error' event, since the docs are not clear about that. (docs say: "The callback is invoked before 'finish' or on error." for node version 15.0.0. Is "on error" how node people say "after error"? idk.)
    stdin.write(
      data,
      (error) => {
        if (!error) resolve(null); // The condition is necessary because when an error occurs, the callback is called before the 'error' event handler
      } // Signal the promise to complete when the write operation is complete with no errors. I don't simply use this `error` parameter because the exception will still be thrown if I don't listen to the 'error' event, and the docs say: "If an error occurs, the callback may or may not be called with the error as its first argument. To reliably detect write errors, add a listener for the 'error' event.". Also, I tested it myself and got two different errors in this callback and in the 'error' event handler.
    );
  });

  return promise;
}

const p = exec("gibberishThatWillFailImmediately");

safelyWriteDataToStdin(p.stdin, veryLargeString).then((error)=>console.log("The error is:", error ));