アダプタ的な関数で非同期コールバックをつなぐ

昨日のエントリでは、「2つのファイルを同時に読み、結果を連結して書き出し、書き出した内容をもう一度読む」という処理を以下のコードで実現しました。

var gate = require('gate');
var fs = require('fs');

var latch = gate.latch();
fs.readFile('path1', 'utf8', latch(1));
fs.readFile('path2', 'utf8', latch(1));
latch.await(function (err, results) { 
  if (err) throw err;
  fs.writeFile('path3', results[0] + results[1], function (err) {
    if (err) throw err;
    fs.readFile('path3', 'utf8', function (err, data) {
      if (err) throw err;
      console.log(data);
      console.log('all done');
    });
  });
});


このコード、1つ気になるところがあるんですよね。それはエラー処理。「if (err) throw err」の処理が散らばっています。コードを読みにくくする上に、一律でエラーハンドリングの方法を変更できないのが欠点です。


そこで手軽に問題を解決する方法を考えました。ずばり、アダプタ的な関数を間にはさめばいいんです。ここではbindと呼びます(F#のワークフローにならって)。このbindはFunction.prototype.bindとは別物です。次のように実装します。

function bind(callback) {
  return function (err) {
    if (err) {
      err.message += ', callbackLocation: ' + err.callbackLocation;
      throw err;
    }
    callback.apply(null, Array.prototype.slice.call(arguments, 1));
  }
}


err.callbackLocationは、gate 0.1.0 で導入したスタック情報です。これがあると並列処理のどれがエラーと関連しているかわかります。
bindを使ってこう書けるようになりました。

var gate = require('gate');
var fs = require('fs');

var latch = gate.latch();
fs.readFile('path1', 'utf8', latch(1));
fs.readFile('path2', 'utf8', latch(1));
latch.await(bind(function (results) {
  fs.writeFile('path3', results[0] + results[1], bind(function () {
    fs.readFile('path3', 'utf8', bind(function (data) {
      console.log(data);
      console.log('all done');
    }));
  }));
}));


どうでしょう、見た目にちょっとすっきり。


bind的な関数は、非同期コールバックを使うときは常に使っておいたほうが便利じゃないかなーと思います、gateを使う使わないに関わらず。ログ出力、エラーハンドリングなどを簡単に一律制御できるので便利です。