「コールバック地獄」からボクらを救ってくれた「Promise」ですが、ふと気が付けば、ちょっと種類の違う別の地獄に落ちてる場合がありますよと。
「なんだPromiseお前もかっ!」的な(笑)
「コールバック地獄」は見た目にネストが深くて「ダメだコリャ感」がわかりやすい。 一方言わば「Promise地獄」は、パッと見スッキリしてるんだけど、少し複雑になると、ホントに正しく動いているのかどうか判別しにくい。 というのも、Promise的に間違っていてもJavaScriptの構文的には正しいことが多々あって、どこでバグっているかがわかりにくい。 結果、変なハマり方をしてしまうんですね。
てことで、今まで自分でハマった「Promise地獄」を思い返して知見をまとめておこうと思います。
Promiseのハンドラーでtry~catchする必要はありません
Promiseオブジェクト生成時のexecutor関数内からエラーが投入されると、そのPromiseはreject
されます。
つまり特別な理由がない限りtry~catch
でエラーを捕まえてreject
する必要はありません。
throw-error-from-ctor.js
function someAsync() { return new Promise(function(resolve, reject) { throw new Error("to reject the promise"); }); } someAsync().then(function() { console.error("OK."); }).catch(function(err) { console.error("Error:", err.message); });
実行結果
$ node throw-error-from-ctor.js Error: to reject the promise $
つまり以下は冗長です。実行結果は上と同じ。
throw-error-redundant.js
function someAsync() { return new Promise(function(resolve, reject) { try { throw new Error("to reject the promise."); } catch(err) { reject(err); } }); }
そしてコレはthenでも同じ
上のことはthenのonfulfilledにも当てはまります。つまりthenの中でtry~catchする必要はありません。 以下のPromiseもrejectされます。
throw-error-from-then.js
function someAsync() { return new Promise(function(resolve, reject) { setTimeout(function() { resolve(); }, 1000); }); } someAsync().then(function() { throw new Error("to reject the promise"); }).catch(function(err) { console.error("Error:", err.message); });
しかしネストした非同期処理はtry-catchで囲むべきです
以下のコードでも、Promiseは結果的にrejectされていますが、投げられたエラーがキャッチされておらず、スタックトレースがプリントされています。
function someAsync() { return new Promise(function(resolve, reject) { setTimeout(function() { resolve(); }, 1000); }); } someAsync().then(function() { return new Promise(function(resolve, reject) { setTimeout(function() { throw new Error("to reject the promise"); }, 1000); }); }).then(function() { console.error("OK."); }).catch(function(err) { console.error("Error:", err.message); });
実行結果
$ node throw-error-from-then-nest.js throw-error-from-then-nest.js:10 throw new Error("to reject the promise"); ^ Error: to reject the promise at Timeout._onTimeout (throw-error-from-then-nest.js:10:19) at ontimeout (timers.js:365:14) at tryOnTimeout (timers.js:237:5) at Timer.listOnTimeout (timers.js:207:5)
実は予想外の動きでした。rejectされないと思っていたのです。もしかして古いバージョンではrejectされないかと確認してみましたが、少なくともnode 4.8.3ではrejectされていました。 なんにせよ、上のようにネストした非同期処理は、以下のようにtry~catchでエラーをハンドリングして明示的にrejectしたほうが良さそうです。
function someAsync() { return new Promise(function(resolve, reject) { setTimeout(function() { resolve(); }, 1000); }); } someAsync().then(function() { return new Promise(function(resolve, reject) { setTimeout(function() { try { throw new Error("to reject the promise"); } catch(err) { reject(err); } }, 1000); }); }).then(function() { console.error("OK."); }).catch(function(err) { console.error("Error:", err.message); });
そのnew Promise
、ホントに必要?
Promiseチェーンが長くなってくると、その一部分をまとめてPromiseを返す関数として独立させたくなったりしますが、その新しい関数の中でPromiseオブジェクトを生成する必要は多分ありません。
以下のようにthenがフラットにたくさん続くと、それはそれでイラッときます。
someAsync().then(function () { return asyncA1(); }).then(function () { return asyncA2(); }).then(function () { return asyncA3(); }).then(function () { return asyncB1(); }).then(function () { return asyncB2(); }).then(function () { console.log("OK."); }).catch(function(err) { console.error("Error:", err.message); });
非同期処理をAとBでまとめるには、以下のように組み直せば良いのですが、、、
function asyncA() { return asyncA1().then(function() { return asyncA2(); }).then(function () { return asyncA3(); }); } function asyncB() { return asyncB1().then(function() { return asyncB2(); }); } someAsync().then(function () { return asyncA(); }).then(function () { return asyncB(); }).then(function () { console.log("OK."); }).catch(function(err) { console.error("Error:", err.message); });
しかしついつい、以下のように新しいPromiseを作って返すコードを書いてしまうことがあるんです。 間違ってはいないけれど冗長ですし、これが積み重なると、まさに「プロミス地獄」が始まる気がする。
function asyncA() { return new Promise(function(resolve, reject) { asyncA1().then(function() { return asyncA2(); }).then(function () { return asyncA3(); }).then(function() { resolve(); }).catch(function(err) { reject(err); }); }); } function asyncB() { return new Promise(function(resolve, reject) { asyncB1().then(function() { return asyncB2(); }).then(function() { resolve(); }).catch(function(err) { reject(err); }); }); } someAsync().then(function () { return asyncA(); }).then(function () { return asyncB(); }).catch(function(err) { console.error("Error:", err.message); });
まとめ
アレっ?書き始める前はもっとあった気がするんですけどね。
思いついたら随時追記するつもりですー。