色々なNodeのライブラリがそれぞれで好きなPromiseライブラリ使ってるけど、混ぜても大丈夫なのか?ちゃんと相互にthenでチェインしたりエラー捕まえたりできるのか?
という事と、async.eachSeriesのようにURLのリストを1つずつ順番に処理完了するのを待ちながら処理していくのはPromiseでどうやって書くのかな?
というのが気になっていたのでちょっと調べた。


https://github.com/shokai/promise-study

環境

node v0.10.29 + coffee-script v1.8.0
なのでまだPromiseが標準ライブラリに入ってないNode環境


試したPromiseライブラリ

このへんが有名そうだったのでREADMEやサンプルなどを読んで、試した。
でもdeferredはインタフェースがthennableじゃないっぽかったのですぐあきらめた。


結論

then/catchしたいだけならes6-promise使って、
async.jsでやるような高機能な並列・並行処理の制御がしたければbluebird使えばいいと思う。
es6-promiseとbluebirdは混ぜこぜで使ってもちゃんと動いた。

Qはなぜかcatchでエラーが捕まえられなかった(俺が使い方間違ってるかもしれない)


Qが例外をキャッチできない


es6-promiseとbluebirdは混ぜてthen/catchでチェインさせても大丈夫
qはcatchでエラーと取れない
qの中でthrow Errorしてもcatchできなかった

試したコード
## いろいろなPromiseライブラリを使ってみる
## Qだけthrow new Errorが捕まえられなかった

{Promise} = require 'es6-promise'
# Promise = require 'q'
# Promise = require 'bluebird'

# 奇数かどうかをtrue/falseで返すPromise
# 数字じゃない物が渡されたらエラー返す
checkOdd = (num) ->
return new Promise (resolve) ->
if typeof num isnt 'number'
throw new Error "#{num} is not number"
resolve num % 2 is 1


for i in [0,1,2,3,null,5,"かずどん",7,8]
do (i) ->
checkOdd i
.then (res) ->
if res
console.log "#{i} is odd"
else
console.log "#{i} is not odd"
.catch (err) ->
console.error err


1つずつ処理する

HTTP Getして、HTMLからtitleを取り出して、それをmacのsayコマンドで読む。
という3つのPromiseをつなげて1セットの処理として、URLリストを順番に処理していく。
RSSを順番に見ていくクローラー的な処理を想定していて、1つ終わったら3秒待ってから次のURLを見に行くようにした。

試したコード
ちなみにURLリスト全部一気に処理するversionもある


下の方でBluebird.eachを使って1つずつURLを処理していく。
thenのチェインが最後まで走ったら3秒待ってから次のURLを処理するし、どこか途中でエラーが起きたらcatchして5秒待って次のURLの処理に行く。とにかく同時に複数のHTTPリクエストは送らない。
こういうのをasync.eachSeriesで書くとけっこう頭が疲れるコードになると思うけどPromise使ったらすんなり書けた。

es6-promiseとbluebirdを混ぜて使っているけどちゃんと動いた。

## HTMLを(1つずつ)取得してtitleを取り出してsayで読み上げる
## いろいろなPromiseライブラリを混ぜて使ってみる
## HTTPリクエストするのは3000 msecごと
## 途中でエラーがあったら5000 msec待ってから、次のHTTPリクエストする

request = require 'request'
cheerio = require 'cheerio'
{exec} = require 'child_process'

process.env.DEBUG ||= '*'
debug = require('debug')('promise-study')


{Promise} = require 'es6-promise'
Q = require 'q'
Bluebird = require 'bluebird'

urls = [
'http://shokai.org'
'そんなURLはない' # URLじゃない文字列
'https://github.com'
'https://github.com/robots.txt' # HTMLが返ってこない文字列
'https://google.co.jp'
]

# HTML本文を取得するPromise
# URLが間違っていたりすると失敗する
getHtml = (url) ->
debug "getHtml(#{url})"
return new Promise (resolve, reject) ->
request url, (err, res, body) ->
if err or res.statusCode isnt 200
reject(err or "statusCode: #{res.statusCode}")
resolve body

# HTMLからタイトルを取得するPromise
# HTMLじゃなければ失敗する
getTitle = (html) ->
debug "getTitle(html)"
return new Bluebird (resolve, reject) ->
$ = cheerio.load html
if title = $('title').text()
resolve title
return
reject 'title not found'

# 音声読み上げするPromise
speech = (txt) ->
debug "speech(#{txt})"
return new Promise (resolve, reject) ->
exec "say #{txt}", (err, stdout, stderr) ->
if err
reject(txt)
return
resolve(txt)

# URLリストをeachで1つずつ処理する
Bluebird.each urls, (url) ->
getHtml url
.then getTitle
.then speech
.then (title) ->
return new Promise (resolve) ->
debug "wait 3000 msec"
# 3秒待ってから次のURLの処理へ
setTimeout ->
debug "wait done"
debug "!!OK #{url} - #{title}"
resolve()
, 3000
.catch (err) ->
return new Promise (resolve, reject) ->
debug "!!ERROR #{url} - #{err}"
debug "wait 5000 msec for Error"
# どこかでエラーあったら5秒待つ
setTimeout ->
debug "wait done"
resolve()
, 5000


実行結果

ちゃんと3秒/5秒待って次、と順番に処理できている。
requestにURLじゃないのを渡した時の例外もtry catch書かずにpromiseのcatchで捕捉できている。