JavaScript
Node.js

promisify についてのまとめ

もうだいぶ前の話になるが、Node.js v8.0 で util.promisify が実装された。
同じく v8.0 で実装された async/await と一緒に使うと非常に便利。

今回は、promisify の便利さを説明するための資料としてのまとめ。

ちなみに Promise については説明しない。

以下のような、何らかのデータベースにクエリ文字列を投げて結果を取得するようなコードがあるとする。

const db = new DB('db://user:pass@host');

db.execQuery('SELECT ... FROM ...', (err, results) => {
  if (err) console.error(err);
  else console.log(results);
});

結果をコールバック関数で取得してくる古き良き (?) 非同期処理だが、このままだと非同期処理をチェーンしたい場合にいわゆるコールバック地獄が発生したり、他の Promise のチェーンに組み込むのが面倒くさかったりする。

手動で Promise 化

よくやるのは、コールバック関数で実装されている非同期処理を自分で Promise を返すようにラップする。

const db = new DB('db://user:pass@host');

function execQuery(query) {
  return new Promise((resolve, reject) => {
    db.execQuery(query, (err, results) => {
      if (err) reject(err);
      else resolve(results);
    });
  });
}

この例だと execQuery を Promise を返す関数としてラップしたので、以下のように使えるようになる。

execQuery('SELECT ... FROM ...')
  .then(results => console.log(results))
  .catch(err => console.error(err));

また、async/await を使うと以下のように書ける。

async function main() {
  const results = await execQuery('SELECT ... FROM ...');
}

しかしこのままでは、Promise に対応していない非同期関数の数だけラップした関数を実装しなければならない。

ラップする際にやることはどの関数もだいたい一緒なので、数が多いとどうしてもコードが冗長になってしまう。

util.promisify を利用

util.promisify を使うと、上記でやったことと同様の Promise 化を簡潔に書くことができる。

ただし適用できる関数には制約があり、以下の2つの条件を満たしている必要がある。

  • 引数の最後でコールバック関数をとる
  • コールバック関数がとる引数の最初の引数はエラー

これを満たしていない関数は util.promisify で Promise 化できないので、手動で Promise 化しなければならない。

今回の例の db.execQuery 関数はこの条件を満たしているので、以下のように Promise 化できる。

const util = require('util');

const db = new DB('db://user:pass@host');

const execQuery = util.promisify(db.execQuery).bind(db);
execQuery('SELECT ... FROM ...')
  .then(results => console.log(results))
  .catch(err => console.error(err));

注意点としては、Promise 化された関数はレシーバが束縛されていないので、必要に応じて .bind(db) のように束縛してから使う必要がある。

(オマケ) 簡易 promisify を自分で実装してみる

util.promisify を使わずとも簡易的なものであれば自分で簡単に実装することもできる。

function promisify(original) {
  return function(...args) {
    return new Promise((resolve, reject) => {
      original.bind(this)(...args, (err, ...values) => {
        if (err) reject(err);
        else resolve(...values);
      })
    })
  }
}

...スプレッド演算子

const db = new DB('db://user:pass@host');

const execQuery = promisify(db.execQuery).bind(db);
execQuery('SELECT ... FROM ...')
  .then(results => console.log(results))
  .catch(err => console.error(err));

参考