煽り気味タイトルですみません。
Livesense Advent Calendar 2016 9日目を担当する堀です。
promiseを業務で使うなかで躓いた点を問題形式にして解説してみました。
参考になれば幸いです。
問題
いきなりですが問題です。
Promise.reject('error occured') // new Promise((done, reject) => reject('error occured'))と同じです
.then(res => console.log('当然実行されない'))
.catch(err => console.log('当然実行される'))
.then(res => console.log('問題1: ここは表示されるでしょうか?'))
Promise.reject('error occured')
.catch(error => console.log('error handling'))
.catch(error => console.log('問題2: ここは表示されるでしょうか?'))
.then(res => console.log('問題3: ここは表示されるでしょうか?'))
解けましたか?第2問です
const failedPromise = Promise.reject('error occured')
failedPromise
.catch(error => { console.log('error'); return error })
.catch(error => console.log('問題4. ここは実行されるか?'))
failedPromise
.catch(error => { console.log('error'); return new Error() })
.catch(error => console.log('問題5. ここは実行されるか?'))
failedPromise
.catch(error => { console.log('error'); throw new Error() })
.catch(error => console.log('問題6. ここは実行されるか?'))
failedPromise
.catch(error => { console.log('error'); return Promise.reject() })
.catch(error => console.log('問題7. ここは実行されるか?'))
最後、おまけの第3問
const successPromise = Promise.resolve('success')
const failedPromise = Promise.reject('error')
Promise.all([
failedPromise,
failedPromise,
successPromise
])
.then(res => console.log('問題8: ここは実行されるでしょうか?されるとしたらresの中身は?', res))
.catch(err => console.log('問題9: ここは実行されるでしょうか?されるとしたらerrの中身は?', err))
Promise.all([
failedPromise.catch(e => 'error1'),
failedPromise.catch(e => 'error2'),
successPromise.then(res => 'success!')
])
.then(res => console.log('問題10: ここは実行されるでしょうか? されるとしたらresの中身は?', res))
.catch(err => console.log('問題11: ここは実行されるでしょうか? されるとしたらerrの中身は?', err))
問題は以上です。
解答は下記の通りです。自分でもぜひ実行してみてください。
問題1: 表示される
問題2: されない
問題3: される
問題4: されない
問題5: されない
問題6: される
問題7: される
問題8: されない
問題9: される, 結果: (ただの)error
問題10: される, 結果: [ 'error1', 'error2', 'success!'](順不同)
問題11: されない
解説
第3問は毛色が違うので、最初の2問について解説していきます。
ES2015のpromiseはpromsies/A+に準拠するようなので、そのドキュメントから考えていきましょう。
一部概要をつかむことを優先させた為に、厳密さに欠ける部分ありますがご容赦ください。
前提の確認
まず前提として、promises/A+には次の3つのstate
1. pending: まだ結果が確定していない状態。下記2つのどちらかに変化する
2. fulfilled: 正常に値が確定したことを表し、内部にその値をもつ
3. rejected: 何らかのエラーがあったことを表し、そのエラーを内部に持つ
があり、下記のような2引数を取るthenメソッドを持ちます。
promise.then(
onFulfilled, // promiseがfulfilledのときに実行される
onRejected // promiseがrejectedのときに実行される
)
とりわけ、ES2015では下記のようにthenとcatchを使います。
promise.then(onFulfilled) //promiseがfulfilledのときに実行される
promise.catch(onrejected) //promiseがrejectedのときに実行される
問題の整理
上記から、第1問、第2問の焦点は次のように表現できます。
-
rejected.catch()
の戻り値は何か? -
fulfilled.then()
または、rejected.catch()
でrejectedを返す方法は何か?
なお、ES2015のcatch(callback)は下記の関係
promise.catch(callback) => promise.then(_, callback)
にありますので、今後thenに統一して考えていきます。
thenメソッドの振る舞い
thenメソッドは2つの引数をとり、その全体像は概ね下記の擬似コードで表せます。
class Promise = Fulfilled(v) | Rejected(r) | Pending
class Promise {
then(onFulfilled, onRejected): Promise = { // 戻り値はPromise
// 呼び出し元のstateに応じた関数が実行される
try {
val functionExecuted =
this match {
case Fulfilled(v) => onFulfilled(v)
case Rejected(r) => onRejected(r)
}
} catch {
// 関数実行時に例外があった場合、その例外を値としてRejectedを返す
e => return new Rejected(e)
}
return promiseResolutionProcedure(functionExecuted)
}
}
まず、promiseのstateに応じて、適切な方の関数が実行されます。
この関数の戻り値はpromiseResolutionProcedureに渡されます。もし関数の実行段階で例外が投げられた場合、そのeを値としてrejectされます。
次にpromiseResolutionProcedureを見ていきましょう。
厳密な振る舞いはドキュメントを参照して頂くとして、概ね下記のように動きます。
def promiseResolutionProcedure(value) {
match {
value: Promise => value
// 本当はここでthenメソッドを持っていないか確認する
value: _ => new Fulfilled(value)
}
}
渡されたvalueがpromiseならそのままを返します。
promiseでない場合、そのvalueがthenメソッドを持たないならonFulfilled経由でもonRejected経由でもvalueを値としてもつfulfilledが返されます。
結論
以上から今回の問題の焦点だった部分をまとめると下記のようになります。
1. rejectedは、catchされた結果(例外を投げなければ) fulfilledに戻る
2. 逆にPromise.then(f1, f2)の戻り値としてrejectedを返したいときは
a. 実行される方の関数で例外を投げる
b. 実行される方の関数でrejectedを返す
少し業務的な話
failedPromise.catch(_function).catch(_function)
とか使う事あるのかよと思うかもしれませんが、
失敗を想定しているテストコードなどで使えます。
it('hogehoge', done => {
handler(expectError)
.catch(err => { assert(error == someErrorType); done() })
.catch(err => done(err))
}
明日は
spycさんのアドベントカレンダー10日目の記事です。
お楽しみに。