壊れたPromise?
エラーのドロップ、結果の消失…
JavaScriptのPromiseは壊れているのか?
昔々
Promiseに関する最も一般的な神話のひとつが、エラー処理の所谓的な欠点です。
何年も前、Promiseのエラー処理は実際にお粗末でした。これを修正するために多くの作業が行われました。
そして見よ、それは修正され、広く普及したのです。
人々は喜んだ
そして悲しいことに、一部の人々は気づきませんでした。
現代
その神話は今も残っており、至る所で目 にします:Mediumの人気記事、DZone、そして多くの他の情報源で。
「公式」のリソースやドキュメントでさえ、主に貧弱な例と悪い習慣を提供しているのは認めましょう。これらはPromiseに対する否定的なケースを「証明」するためによく使われます。状況をさらに悪化させる「治療法」を提案するものさえあります。(注:リンクは削除済み)
トラブルを避けるためのルール
- Promiseは掴まるものが必要
- 関数から常に
returnする。
- 関数から常に
- 本物の
Errorインスタンスを使う- 常に
Errorインスタンスを使う。
- 常に
- 意味のある場所でエラーを処理する
- 少なくとも一度は
.catch()を常に使う。
- 少なくとも一度は
- 名前付き関数で明確さを加える 🦄✨
- 名前付き関数を推奨する。
#1 Promiseは掴まるものが必要
関数から常にreturnすることは極めて重要です。
Promiseのコールバック関数は、.then(callback)や.catch(callback)において特定のパターンに従います。
返された値は次の.then()のコールバックに渡されます。
function addTen(number) { return number + 10;}
Promise.resolve(10) // 10 .then(addTen) // 20 .then(addTen) // 30 .then(addTen) // 40 .then(console.log) // "40" と出力「常にreturnする」ボーナス:コードのユニットテストがはるかに容易になります。
質問: いくつの異なるPromise状態(resolvedとrejected)が作成されましたか?
質問: 前の例でいくつのPromiseが作成されましたか?
#2 本物のErrorインスタンスを使う
JavaScriptにはエラーに関して興味深い動作があります(これは非同期および同期コードの両方に適用されます)。
[repl.itで例を見る:throwing errors in javascript]
行番号やコールスタックに関する有用な詳細情報を取得するためには、Errorインスタンスを使う必要があります。文字列をthrowすることはPythonやRubyのように動作しません。
JavaScriptはthrow "string"を処理できるように見えますが、catchハンドラで文字列が表示されます。しかし、表示されるのはそのデータだけです*。以前のスタックフレームは含まれません。
正しいnew Errorの例:
throw new Error('message') // ✅Promise.reject(new Error('message')) // ✅throw Error('message') // ✅Promise.reject(Error('message')) // ✅以下は一般的なアンチパターンです:
throw 'error message' // ❌Promise.reject(-42) // ❌#3 意味のある場所でエラーを処理する
Promiseは.catch()を使ったエレガントなエラー処理方法を提供します。これは基本的に特殊な.then()の一種で、先行する.then()からのエラーがここで処理されます。例を見てみましょう…
Promise.resolve(42) .then(() => 'hello') .catch(() => console.log('will not get hit')) .then(() => throw new Error('totes fail')) .catch(() => console.log('WILL get hit')).catch()はDOMイベントハンドラ(例:click、keypress)のように見えるかもしれませんが、その配置が重要です。自分より上でthrowされたエラーしか「キャッチ」できないからです。
エラーの上書きは比較的簡単です。.catch()のコールバックで非エラー値を返すと、Promiseチェーンは.then()のコールバックを順に実行するモードに切り替わります。(実質的に。)
次の例のシーケンスを追ってみてください:
Promise.resolve(42) .then(() => 'hello') .then(() => throw new Error('totes fail')) .catch(() => { return 99 }) .then(num => num + 1) .then(console.log) // 期待される出力: 100理解すべき重要なのはシーケンスです。
これはばかげた例ですが、Promiseにおけるエラーとデータのフローを説明するために設計されています。
シーケンスの概要は以下の通りです:
- 42が初期値。
- 次のメソッドによって常に
helloが返される。 - 前の値を無視し、
'totes fail'メッセージでエラーをthrowする。 .catch()がエラーをインターセプトし、代わりに99を返し、後続の.then()で処理される。numをインクリメントして100を返す。console.logメソッドが100を受け取り、出力する! :tada:
質問: 2つの.catch()が連続している場合、何が起こりますか?2つ目が実行されることはありますか?ユースケースが思いつきますか?
質問: .catch()はどのようにしてエラーを無視できますか?Promise.allのエラーによる早期終了をどのように防げますか?
#4 名前付き関数で明確さを加える 🦄✨
次の2つの例の可読性を比較してください:
無名関数: ❌
Promise.resolve(10) // 10 .then(x => x * 2) // 20 .then(x => x / 4) // 5 .then(x => x * x) // 25 .then(x => x.toFixed(2)) // "25.00" .then(x => console.log(x)) // 期待される出力: "25.00"名前付き関数: ✅
Promise.resolve(10) // 10 .then(double) // 20 .then(quarter) // 5 .then(square) // 25 .then(format) // "25.00" .then(log) // 期待される出力: "25.00"
const double = x => x * 2const quarter = x => x / 4const square = x => x * xconst format = x => x.toFixed(2)const log = x => console.log(x)ボーナス: ✅
配列メソッド互換!!!
名前付き関数はArray.prototype.の仲間たちと再利用できます。.map()、.filter()、.every()、.some()、.find()を含む!
コレクションパイプライン #FTW:
// まったく同じものだ :mindblown:
[10, 20] // [ 10, 20 ] .map(double) // [ 20, 40 ] .map(quarter) // [ 5, 10 ] .map(square) // [ 25, 100 ] .map(format) // [ "25.00", "100.00" ] .map(log) // 期待される出力: "25.00", "100.00" の2行このリニアスタイルのコーディングをしたくない場合…シンプルな関数があります!
必要に応じて使えます:
// ネストパターン// ❌ ただし、これはやらないでください
const result = format(square(quarter(double(10))))
log(result)// 期待される出力: "25.00"なぜ関数のネストがアンチパターンなのか?
- 多くの人にとって読みやすくない
- git diffで誰が何を変更したかがすぐにわからない
- ネストされた関数の途中からデバッグやログ出力が難しい