Promise.tryでより明瞭な非同期チェーンを書く
同期的に動作するかもしれない、あるいは非同期的に動作するかもしれない関数から始まるPromiseチェーンを書いたことがあれば、おそらく厄介な問題に遭遇したことがあるでしょう:.catch()をどこに配置すべきか?
Promiseが返される前に投げられた同期エラーは、すでにPromiseコンテキスト内にいない限り、.catch()でキャッチされません。Promise.try()は、呼び出している関数が同期、非同期、あるいはその中間であるかに関わらず、単一で一貫したエントリーポイントを提供することで、この問題をクリーンに解決します。
重要なポイント
- 時々Promiseを返す関数内の同期的なthrowは、
.catch()を完全にすり抜け、未処理の例外につながる可能性があります。 Promise.try()は関数を即座に実行し、同期的な戻り値と同期的なthrowの両方を適切なPromiseにラップし、統一されたエラー処理パスを提供します。Promise.resolve(fn())とは異なり、同期エラーをキャッチします。Promise.resolve().then(fn)とは異なり、マイクロタスクへの遅延なしに即座に実行します。- 同期/非同期が混在するエントリーポイントを持つ
.then()ベースのチェーンに最適です —async/awaitの置き換えではありません。
問題:.catch()チェーンをすり抜ける同期エラー
条件に応じてキャッシュから同期的に読み取るか、APIから非同期的に取得するデータローダーを考えてみましょう:
function loadData(key) {
const cached = getFromCache(key) // may throw synchronously
if (cached) return cached
return fetch(`/api/data/${key}`).then(res => res.json())
}
loadData('user-1')
.then(data => render(data))
.catch(err => handleError(err)) // ⚠️ Won't catch sync throws from loadData
getFromCacheが同期的にthrowした場合、そのエラーは.catch()によって決してキャッチされません。throwはPromiseが存在する前に発生するため、チェーン全体をすり抜けて未処理の例外になります。
ここには注目に値する2つ目の微妙な点があります:loadDataがキャッシュされた値を直接返す場合(thenableでない)、プレーンな値には.then()メソッドがないため、それに対して.then()を呼び出すことも失敗します。この関数は本質的に脆弱です — 一方の分岐ではPromiseを返し、もう一方では生の値を返します。Promise.try()は常にPromiseを生成することで、両方の問題に対処します。
Promise.try()がこれを修正する方法
Promise.try(fn)は提供された関数を即座に実行し、結果をPromiseでラップします。関数がプレーンな値を返す場合、その値でresolveします。Promiseを返す場合、そのPromiseを採用します。同期的にthrowした場合、そのエラーをrejectionに変換します。
これにより、同期エラーと非同期エラーの両方を同じ.catch()で処理するための単一のエントリーポイントが得られます:
Promise.try(() => loadData('user-1'))
.then(data => render(data))
.catch(err => handleError(err)) // ✅ Catches both sync throws and async rejections
特別なケース分けは不要です。チェーンを開始する前にtry/catchでラップする必要もありません。すべてが期待通りに.catch()を通過します。
Discover how at OpenReplay.com.
Promise.resolve().then(fn)およびPromise.resolve(fn())との違い
これら2つのパターンは回避策としてよく使用されますが、重要な点で異なる動作をします。
**Promise.resolve(fn())**はfn()を即座に呼び出しますが、Promiseコンテキストの外側で実行されます。ここでの同期的なthrowは、rejectionではなく未キャッチの例外になります。
**Promise.resolve().then(fn)**はfnの実行をマイクロタスクに遅延させます。つまり、fnは即座に実行されません — これは、即座の実行が必要な場合に微妙なタイミングの問題を引き起こし、動作の予測可能性を低下させる可能性があります。
**Promise.try(fn)**はfnを即座に実行し、かつ同期的なthrowをrejectionとしてキャプチャします。Promiseチェーンを開始するための3つの中で最も予測可能です。
| パターン | fnを即座に実行 | 同期throwをキャッチ |
|---|---|---|
Promise.resolve(fn()) | ✅ | ❌ |
Promise.resolve().then(fn) | ❌ | ✅ |
Promise.try(fn) | ✅ | ✅ |
実践的なフロントエンドのユースケース
Promise.try()は、動作が実行時の条件に依存するJavaScript非同期パターンに自然に適合します:
ユーティリティ関数がキャッシュされたデータを同期的に返すか、非同期的に取得する場合:
Promise.try(() => getUserFromCacheOrAPI(userId))
.then(updateUI)
.catch(showErrorBanner)
条件付き非同期ワークフローで、バリデーションステップが非同期処理が始まる前にthrowする可能性がある場合:
Promise.try(() => {
validateInput(formData) // throws if invalid
return submitForm(formData) // returns a promise
})
.then(handleSuccess)
.catch(handleError)
ブラウザサポートと互換性
Promise.try()はECMAScript 2025(ES2025)に含まれ、Chrome 128+、Firefox 134+、Safari 18.2+、Node.js 22.7.0+でサポートされています。現在のブラウザサポート状況はCan I Useで確認できます。これは主要なブラウザとランタイム全体での実装状況を追跡しています。
古い環境では、シンプルなラッパーでポリフィルできます:
Promise.try = Promise.try || function(fn) {
return new Promise(resolve => resolve(fn()))
}
これが機能するのは、new Promiseのexecutorが同期的に実行されるため、fn()が即座に呼び出されるからです。fn()がthrowした場合、Promiseコンストラクタがそれをキャッチしてrejectionに変換します。fn()がthenableを返す場合、resolveがそれを採用します。
まとめ
Promise.try()はasync/awaitの置き換えではありません。これは1つの特定の状況のための小さく焦点を絞ったツールです:エントリーポイントが同期的にthrowするか、値とPromiseの混合を返す可能性がある場合にPromiseチェーンを開始することです。
すでにasync関数内にいる場合、try/catchブロックが両方のケースを自然に処理します。しかし、.then()チェーンで作業している場合 — 特に条件付きロジックを持つユーティリティ関数やデータローダーの周辺では — Promise.try()はエラー処理を一貫させ、チェーンをクリーンに保ちます。
よくある質問
はい。Promise.try()に渡す関数がasyncの場合、それはPromiseを返し、Promise.try()はそのPromiseを採用します。これはPromiseを返す任意の関数を渡すのと同じように機能します。Promise.try()の主な利点は、Promiseを全く返さない可能性がある関数、または返す前にthrowする可能性がある関数のためのものです。
いいえ。async関数内では、標準のtry/catchブロックがすでに同期的なthrowとawaitされたrejectionの両方をキャプチャします。Promise.try()は、安全なエントリーポイントが必要な.then()スタイルのチェーン用に設計されています。すでにasync/awaitを使用している場合、Promise.try()は必要ない可能性が高いです。
new Promise(resolve => resolve(fn()))を使用する一般的なポリフィルは、ネイティブ実装と機能的に同等です。fnを即座に実行し、Promiseコンストラクタを介して同期throwをキャッチし、resolveを通じてthenableを採用します。ネイティブサポートがない環境での本番使用に安全です。
Promise.resolve(fn())はPromiseコンテキストの外でfnを呼び出すため、同期throwは未キャッチの例外になります。Promise.resolve().then(fn)はthrowをキャッチしますが、実行をマイクロタスクに遅延させるため、fnは即座に実行されません。Promise.try(fn)は、fnを即座に実行し、かつ同期エラーをrejectionとしてキャプチャする唯一のパターンです。
Complete picture for complete understanding
Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.
Check our GitHub repo and join the thousands of developers in our community.