util.promisify が追加された
Node.js のコアに util.promisify
が追加された。
github.com
今回は util.promisify
が持つ役割を中心に Node.js における Promise の立場についても話していけるといいと思う。
util.promisify
とは
読んで字のごとく関数を Promise に変換してくれるユーティリティメソッド。 下記のような要領で変換できる。
const util = require('util'); const fs = require('fs'); const stat = util.promisify(fs.stat); stat('.').then((stats) => { console.log(stats); }).catch((error) => { console.error(error); });
async-awaitを使いたい場合(Node.js v7の最新では既にenabled)は下記の通り
const util = require('util'); const fs = require('fs'); const stat = util.promisify(fs.stat); async function callStat() { try { const stats = await stat('.'); console.log(stats.uid); } catch(e) { console.error(e); } }
util.promisify 注意事項
Node.js のコアメソッドに限らず、他のメソッドもPromiseに変換できる、ただし、変換する場合はその関数がpromisifyの規約に従っている必要がある。
その規約というのは、
- コールバック関数を引数の最後に取ること (
function(arg1, arg2, cb){}
) - コールバック関数の最初の引数はエラーであること (
cb(err, res)
)
である、これに従わない関数の場合はうまくPromisifyされないので要注意、特に2つ目の規約に違反しているとエラーじゃないものがPromise.reject
の対象になってしまうことがある。
Node.js のコアメソッドのほとんどは上記の規約に従ったコールバック関数を取るが、Node.jsの規約から外れたコールバックの使い方をしているメソッドの場合は一工夫が必要になる。
例えば、 setTimeout
や setImmediate
の場合がそうなる、これらはコールバック関数を最初の引数に要求するし、コールバック関数の最初の引数はエラーとは限らない。
こういった関数を Promise に変換したい場合は util.promisify.custom
をプロパティにしてカスタマイズされたPromisify関数を提供してあげる必要がある。
const test = function(cb, arg){ cb(arg) } test[util.promisify.custom] = (arg) => { return new Promise((resolve, reject) => { test(resolve, arg); }) }; const p = util.promisify(test); p('foo').then((arg) => console.log(arg));
Node.js の setTimeout
や setImmediate
はこの util.promisify.custom
を使ってcustomizeされたPromisifyを作っている。
Node.js における Promise の位置付け
フロントエンドでは async await が採用されたり、 Web 標準のAPIが採用していたりと、ほぼスタンダードな印象を受ける Promise だが、 Node.js の中では実はまだまだ議論の余地がある。
util.promisify
を採用するかどうかを議論していた時にその場に居たので、要点をまとめると
unhandledRejectionの時の振る舞いが決まっていない
という一点につきる。
議論で話している感じは以下の通り:
- Promise のニーズは高い、特に async await のような構文サポートまであるので強力
- しかしながら、Promise の unhandledRejection が起きた時にNodeのデフォルトをどう動かすようにするかが未定
- 現時点では warnings が出る。しかし、今やってる unhandledRejection はただ単に例外発生時に
.catch
をする Promise が その時点で いなかっただけであり、Promiseが例外をキャッチするのは仕様上いつでも良いので、この時点で出るwarningsとしては適切ではない(非同期にキャッチされる可能性があるため)。 - 現在の仕様で Promies を使ったとして、例外をキャッチしなかった際に容易にメモリリークやファイルディスクリプタのリークが起きることは想像しやすく、やはりNodeコアの中でも簡単にリークが作り込めてしまうような状況にするべきではない、リークが気づきにくくなる位なら異常終了した方がマシ
というのが議論ポイントだった。
もう少し噛み砕くと、『 Promise を簡単に使えるようにする(util.promisifyを提供する)なら、 Promise を安全に使える手段として提供してあげる(リークを起こさないようにする)べき』という感じだろうか。
これに対しては現時点で提案中のデフォルトの unhandledRejection の動きで既に3,4候補存在する。
unhandledRejection
が起きたら:
- GCが起きるまで待ち、起きた時にunhandledな状態のPromiseがあったら異常終了 (https://github.com/nodejs/node/pull/12010)
- process.nextTickでそのtick内で例外が回収されてなかったら異常終了 (https://github.com/nodejs/node/pull/12734)
- フラグ付きでエラーをスローする(–throw-on-unhandled-rejection)として、デフォルトは現時点の動きそのまま (https://github.com/nodejs/node/pull/6355)
まだこの部分の議論は続いている。
Node.js Collaborators Summitにおける、約一時間の議論は発散して終わった感じがするが、 util.promisify
を追加する件に関してはある程度の有用性、コアでやることの意義が認められてマージされた。
Promise とどう付き合っていくか
僕らアプリケーションをNode.jsで書いている側としては気をつけるべきなのは、 Promise が使いやすくなってきているが、まだまだ運用面での知見が少ないという点だと思う。
もしかしたらリークが起きてるけど気づいていないとか、例外がスローされていたけどそのまま放置されていたとかそういう事がないように Promise は気をつけて使うべきだろう。
実際に Node.js v4 では Promise でメモリリークが起きていた(現在は修正済み)
更に言うと、現時点の Promise には core-dump を出す仕組みもない(processが死んだ時の --abort-on-uncaught-exception
相当)。Promiseを使ってしまうとエラーになって死んだ時に解析がしにくいという側面もある。
util.promisifyができたことで、Promiseが使いやすくなっているが、この辺りはまだ仕様検討中なのでv8.0次第ではPromiseの使い勝手は変わる可能性もある。
2017/05/12 追記: Promiseを無限ループさせるとリークが起きるというのは仕様の問題であって、Node.jsの問題ではありませんでした。
まとめ
- util.promisify の説明
- Promise と Node の位置付け
- Promise とどう付き合っていくか