読者です 読者をやめる 読者になる 読者になる

エンタープライズIT系エンジニアのぼやっきー

特にまとまりもなく、色々なことをぼやきます。最近はオープンソースの業務系システムに興味あり。

今更ながらNode.jsでPromiseの使用例

今更ながらこの話題を。使い方を間違えるので、私としては co は非推奨。

Node.js でAPIなどを作成するときに、どうしてもDBアクセス等で callback の利用が多くなります。
特に複雑な業務システム系ならばなおさらです。
また、エラーハンドリングの記述が多くなり、ハンドリング漏れの恐れがあります。

そんな時に「Promise」「then」「catch」を使います。

// こんな感じでPromiseオブジェクトを生成
var promise = new Promise(function(resolve, reject){
  // 非同期処理
  setTimeout(function(){
    try {
      var result = hoge();
      resolve(result);
    } catch(err) {
      reject(err);
    }
  }, 1000);
});

// コールバックに相当する部分
// thenはチェインできるので、上記にそのまま .then で繋げてもOK
promise.then(function(result){
  //引数はresolveの引数がそのまま渡されてくる
  console.log(result);
});

// reject または エラーが throw された場合に呼ばれる部分
promise.catch(function(err){
  console.log(err);
});

まずはこれが基本形です。

次にちょっと応用してみます。

var promise1 = new Promise(....略).then(...略).catch(...略);
var promise2 = Promise.resolve().then(...略);

// 1. 順序を保証して実行
var serial = Promise.resolve().then(function(){
  return promise1;
}).then(function(promise1_result){
  return promise2;
});

// 2. 順序に関わらず実行
var parallel = Promise.all(
  [promise1, promise2]
).then(function(result){
  console.log(result); // [promise1_result, promise2_result]
});

1.の場合はpromise1の次にpromise2がコールされます。
もし、promise1でエラーが発生して、catchが呼ばれても次の「then」の中にあるpromise2が呼ばれます。
そのため、この「then」は try-catch-finally の finally に相当させることができます。

2.の場合はpromise1とpromise2が順序を問わず実行されます。
片方の終了を待たず、時多重分割でお互いに割り込みのタイミングがあれば割り込みながら実行されていきます。

なお、さらりと出してしまいましたが、以下は同等です。

// 何もせずにresolve()がコールされたPromiseオブジェクト
var promise1 = new Promise(function(resolve, reject){
  resolve();
});

// 何もせずにresolve()がコールされたPromiseオブジェクト
var promise2 = Promise.resolve();

// 何もせずにreject()がコールされたPromiseオブジェクト
var promise3 = new Promise(function(resolve, reject){
  reject();
});

// 何もせずにreject()がコールされたPromiseオブジェクト
var promise4 = Promise.reject();

Promiseで最も重要なのは、thenの使い方です。

例えば、以下のような形。

悪い例

Promise.resolve().then(function(){
  Promise.resolve().then(function(){
    throw new Error();
  };
}).catch(function(err){
  console.log(err);
});

良い例

Promise.resolve().then(function(){
  return Promise.resolve().then(function(){
    throw new Error();
  };
}).catch(function(err){
  console.log(err);
});

良い例は then 内で Promise を return しています。
これによって、throw new Error() された際に catch で捕まえることができるようになります。

何が言いたいかというと、以下のような書き方ができるということです。

var conn = null;
var transaction = new Promise(function(resolve, reject){
  // データベースのコネクションを取得する
  ConnectionPool.getConnection(parameters, function callback(err, connection){
    if(err){reject();}
    conn = connection;
    resolve();
  });

}).then(function(){
  // 何かしらQueryを流して、その後の処理を実行とか
  return new Promise(function(resolve, reject){
    conn.query(...省略)
  });

}).then(function(){
  // 問題なくここまでくればcommit。
  if(conn) {
    conn.commit();
  }

}).catch(function(err){
  console.log(err);
  // エラーがcatchされたらrollback。
  if(conn) {
    conn.rollback();
  }

}).then(function(){
  // 最後に必ずコネクションを解放する
  if(conn) {
    ConnectionPool.release(conn);
  }
});

then の中で reject したい

then の中は Error が throw されれば reject 扱いとなります。
しかし、想定されるエラーや強制的に reject をコールしたい場合は以下のようにしたほうが良いと思っています。
個人的に読みやすいですし、then の中も Promise であると考えると納得感があります。

Promise.resolve().then(function(){
  if(err) {
    return Promise.reject(err);
  }
});

使ってみた感想

co を使えばもっとすっきり書けます。
しかし、co はとりあえず括っておいて、yield 付ければいいんでしょ的な考えに落ちやすくダメコードを書いてしまいます。
例えば、DBコネクションを使いまわしてトランザクションを一気にロールバックしたい時に、function 毎に co を呼んでいることが原因で、上手くエラーハンドリングできないことが発生します。というか、恥ずかしながら私がそうなって泣きながら大量のコードを修正しています。。
それであれば、Promise/then/catchで書いた方が、わかりやすくて個人的に好みです。


[PR] 日本でまともに使えるオープンソースCRMが出ています。
f:id:junmt:20151117120041p:plain
CRM構築はオープンソースのF-RevoCRM -エフレボシーアールエム-