child_processモジュールの関数群もutil.promisify()を使ってPromise化できる。しかしexecFile()などは元々のコールバックに3つ引数が渡されることから少し使い方が違ってくる。ここまでは皆が通る道だろう。実はそれだけでなくリジェクトされた時も少し違うことが公式ドキュメントの該当する項の最後に記載されていた。

In case of an error, a rejected promise is returned, with the same error object given in the callback, but with an additional two properties stdout and stderr

ちゃんとcatch()されているなら、エラーが起きた場合にその中で標準出力と標準エラーにアクセスできるわけだ。こうなっていることでプロミス化したexecFile()awaitしてエラーになった時、その解決に必要になる重要なメッセージを記録でき、実行したファイルが起こしたエラーの解決につなげられる。

const util = require("util");
const { execFile } = require("child_process");
const execFileAsync = util.promisify(execFile);

const getVersion = async () => {
  const { stdout } = await execFile("node", ["--version"]);
  console.log(stdout);
};

getVersion().catch(e => {
  if (e.stderr) {
    console.error(e.stderr);
  }

  console.trace(e);
});

Promiseのcatch()側でやる。これはこれで専用のcatch()を書くことになるのがちょっと気になる。大抵の場合はErrorオブジェクトがいい感じになっているような気がするので、実はあまり考えなくて良いのかもしれない。


僕は当初try..catchでやるものと考えていて、書きづらいな……と感じていた。

const util = require("util");
const { execFile } = require("child_process");
const execFileAsync = util.promisify(execFile);

const getVersion = async () => {
  let stdout;
  let stderr;

  try {
    ({ stdout, stderr }) = await execFileAsync("node", ["--version"]);
  } catch (e) {
    console.error(stderr);
    throw e;
  }

  console.log(stdout);
};

getVersion().catch(e => {
  console.trace(e);
});

letを前出ししたり、そのおかげでawaitで受け取る時に()で括らなければならない。結局はそれ以前の問題で、これではcatch節でstderrが拾えない。