node.js におけるエラー処理のコーディングパターン (もしくは非同期 JavaScript における例外処理)
node.js を代表とする JavaScript を用いた非同期プログラミング環境においては、コーディングパターンのベストプラクティスが共有されておらず、結果として品質の低いコードが多くなるという問題があるように思います。そこで、特にエラー処理をどう書くべきか、既存のライブラリを使う方法を紹介してみることにしました。
いきなりですが、ファイルの文字数を返す関数を作ることを考えてみます。Java だと以下のような感じになるでしょうか。countChars メソッドに注目すると、エラーを例外として扱っていて、モジュラーかつ簡潔になっていることがわかります。
class FileCounter { static long countChars(String filename) throws IOException { FileInputStream is = new FileInputStream(filename); try { long count = 0; while (is.read() != -1) { count += 1; } return count; } finally { is.close(); } } static void main(String args[]) { try { System.out.println(countChars(argv[0])); } catch (IOException e) { System.err.println(e.getMessage()); } } }
では、同様のものを node.js で書くとなると、どうしたらいいでしょう。解決すべき問題は2点あります。
第1の点は、コールバックの第1引数に渡ってくるエラー値をどうやって自動的に処理するかです(node.js では、エラーは各コールバックの第1引数として通知されます)。第2は、エラーが発生した場合、そのエラーをどうやって上位(呼び出し元)に伝播 (propagate) させていくか、という点です。
処理をウォーターフォールで記述可能な async.js のラッパーを使い *1、なおかつ、自分の定義するコールバック全ての第1引数を error 値の受け取りに使うことで、これら2つの問題をきれいに解決することができます。
function countChars(filename, callback) { async.waterfall([ function (next) { fs.readFile(filename, 'utf-8', next); }, function (data, next) { next(null, data.length); } ], callback); } function main(args) { async.waterfall([ function (next) { countChars(args[0], next); }, function (length, next) { console.log(length); } ], function (err) { console.err(err); }); }
この コードを見ていただければ、countChars 関数にはエラー処理が全く含まれていないことがわかると思います。エラーは全て main 関数に伝播されて、そこで処理が行われています。もう少し定式化すると、
async.waterfall([
// この中に書かれるのが try 文に相当する処理
], function (err, ...) {
// ここは catch 文に相当する処理
// catch しないのなら、関数を書かずに、上位の callback を渡す
});
という構造になっているのがわかります。
このように、「全ての」コールバックの第1引数をエラー値の通知に使うものとし、その処理を async.js のようなライブラリを用いて隠蔽することで、エラー処理を必要なところでまとめて書くという、同期的なプログラミング言語の例外処理と同様のモデルを非同期の JavaScript プログラムでも実現することができます。
繰り返しますが、「全ての」コールバックの第1引数を error にすることが重要です。
プログラミングにおいて、エラー処理は見過ごされがちですがとても重要な要素です。以上のようなコーディングパターンを用いれば、エラー処理を簡潔かつ確実に書くことができ、品質の高い JavaScript アプリケーションを実現することができるでしょう。
*1:同様の枠組みがあれば他のラッパーでもいいです