DanLevy.net

壊れたPromise?

エラーのドロップ、結果の消失…

Hero image for 壊れたPromise?

JavaScriptのPromiseは壊れているのか?

昔々

Promiseに関する最も一般的な神話のひとつが、エラー処理の所谓的な欠点です。

何年も前、Promiseのエラー処理は実際にお粗末でした。これを修正するために多くの作業が行われました。

そして見よ、それは修正され広く普及したのです。

人々は喜んだ

そして悲しいことに、一部の人々は気づきませんでした。

現代

その神話は今も残っており、至る所で目 にします:Mediumの人気記事DZone、そして多くの他の情報源で。

「公式」のリソースやドキュメントでさえ、主に貧弱な例と悪い習慣を提供しているのは認めましょう。これらはPromiseに対する否定的なケースを「証明」するためによく使われます。状況をさらに悪化させる「治療法」を提案するものさえあります。(注:リンクは削除済み)



トラブルを避けるためのルール

  1. Promiseは掴まるものが必要
    • 関数から常にreturnする。
  2. 本物のErrorインスタンスを使う
    • 常にErrorインスタンスを使う。
  3. 意味のある場所でエラーを処理する
    • 少なくとも一度は.catch()常に使う。
  4. 名前付き関数で明確さを加える 🦄✨
    • 名前付き関数を推奨する。

#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] 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イベントハンドラ(例:clickkeypress)のように見えるかもしれませんが、その配置が重要です。自分より上で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におけるエラーとデータのフローを説明するために設計されています。

シーケンスの概要は以下の通りです:

  1. 42が初期値。
  2. 次のメソッドによって常にhelloが返される。
  3. 前の値を無視し、'totes fail'メッセージでエラーをthrowする。
  4. .catch()がエラーをインターセプトし、代わりに99を返し、後続の.then()で処理される。
  5. numをインクリメントして100を返す。
  6. 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 * 2
const quarter = x => x / 4
const square = x => x * x
const 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"

なぜ関数のネストがアンチパターンなのか?

  1. 多くの人にとって読みやすくない
  2. git diffで誰が何を変更したかがすぐにわからない
  3. ネストされた関数の途中からデバッグやログ出力が難しい