Promise.try() で同期・非同期エラーをエレガントに扱う
TL;DR: 要するに、try-catch を 1 つ減らせます。
JavaScript の非同期プログラミングの進化において、Promise は常に中心的な存在でした。しかし、コールバック関数が同期的に例外を投げる場合もあれば、非同期に Promise を返す場合もあると、開発者は煩雑なエラーハンドリングを書かされがちです。Promise.try() はこの問題をエレガントに解決するために導入されました。同期エラーと非同期エラーの境界を統一し、関数実行のあらゆる結果を一貫した方法で扱えるようにしてくれます。
Promise.try() とは
Promise.try() は JavaScript の静的メソッドで、同期/非同期、戻り値/例外のいずれであっても任意のコールバック関数を受け取り、その結果を Promise として統一的に包みます。つまり、関数が値を即時に返しても、例外を投げても、非同期 Promise を返しても、Promise.try() は常にチェーン可能な Promise オブジェクトを返してくれます。
構文はとてもシンプルです。
Promise.try(func)
Promise.try(func, arg1, arg2, ...etc)
ここで func は呼び出したい関数、arg1、arg2 などはその関数に渡す引数です。
なぜ Promise.try() が必要なのか
日々の開発では、「コールバックが同期実行か非同期実行か分からないが、結果は同じ形で扱いたい」という厄介な場面に頻繁に出会います。次の例を見てみましょう。
// Promise.resolve で関数呼び出しを包もうとする例
function runCallback(callback) {
return Promise.resolve(callback())
}
この書き方は簡潔に見えますが、致命的な欠点があります。callback() が同期的に例外を投げた場合、そのエラーは Promise の reject として捕捉されず、外側へそのまま投げられてしまい、アプリがクラッシュする可能性があります。
従来は次のように書く必要がありました。
function runCallbackSafely(callback) {
try {
const result = callback()
return Promise.resolve(result)
} catch (error) {
return Promise.reject(error)
}
}
このパターンは動作しますが、コードが冗長になり、手動で try-catch を書く必要があります。Promise.try() はこれを 1 行にまとめてくれます。
基本的な使い方と例
基本の使い方
Promise.try() の最も基本的な使い方は、関数呼び出しを Promise に「持ち上げる」ことです。
function doSomething() {
return 'Success!'
}
Promise.try(doSomething)
.then((result) => console.log(result)) // "Success!"
.catch((error) => console.error(error))
同期エラーの処理
関数が同期的に例外を投げた場合、Promise.try() はそれを自動的に捕捉し、Promise の reject に変換します。
function mightFail() {
throw new Error('Something went wrong!')
}
Promise.try(mightFail)
.then((result) => console.log(result))
.catch((error) => console.error(error.message)) // "Something went wrong!"
非同期関数の処理
Promise を返す非同期関数にも、Promise.try() は同じように使えます。
async function fetchData() {
const response = await fetch('/api/data')
return response.json()
}
Promise.try(fetchData)
.then((data) => console.log(data))
.catch((error) => console.error('Fetch failed:', error))
引数を渡す
Promise.try() はコールバックに引数を直接渡せるため、余計なクロージャを作る必要がありません。
function greet(name, greeting) {
return `${greeting}, ${name}!`
}
// 従来の方法: クロージャを作る
Promise.resolve().then(() => greet('World', 'Hello'))
// Promise.try の方法: 引数を直接渡す
Promise.try(greet, 'World', 'Hello').then((message) => console.log(message)) // "Hello, World!"
この方法はコードを簡潔にするだけでなく、不要なクロージャ生成を避けられるためパフォーマンス面でも有利です。
実践的なユースケース
ケース 1:API エラー処理の統一
たとえば、ユーザー定義コールバックを受け取るライブラリを作っているとします。コールバックは同期かもしれないし非同期かもしれません。結果を統一的に扱いたい場合、次のように書けます。
class TaskRunner {
execute(task, ...args) {
return Promise.try(task, ...args)
.then((result) => ({
success: true,
data: result,
}))
.catch((error) => ({
success: false,
error: error.message,
}))
}
}
const runner = new TaskRunner()
// 同期タスク
runner.execute(() => 42).then(console.log) // { success: true, data: 42 }
// 例外を投げる可能性があるタスク
runner
.execute(() => {
throw new Error('Oops')
})
.then(console.log) // { success: false, error: "Oops" }
// 非同期タスク
runner
.execute(async () => {
await new Promise((resolve) => setTimeout(resolve, 100))
return 'Done!'
})
.then(console.log) // { success: true, data: "Done!" }
ケース 2:同期コードに近いエラーハンドリングスタイル
Promise.try() を catch() や finally() と組み合わせると、同期コードに近い読みやすいエラー処理スタイルを実現できます。
function processUserData(userData) {
return Promise.try(() => validateUser(userData))
.then((user) => enrichUserData(user))
.then((enriched) => saveToDatabase(enriched))
.catch((error) => {
console.error('Processing failed:', error)
throw error // 再スローするか、デフォルト値を返す
})
.finally(() => {
console.log('Processing attempt completed')
cleanupResources()
})
}
このようなチェーン構文は、入れ子の try-catch を避けながら、非同期エラー処理を明快にしてくれます。
ケース 3:サードパーティライブラリとの統合
サードパーティライブラリを組み込むと、あるケースでは同期的に例外を投げ、別のケースでは Promise を返す関数に出会うことがあります。Promise.try() を使えばその違いを意識せずに扱えます。
function callThirdPartyAPI(config) {
return Promise.try(() => thirdPartyLibrary.process(config))
.then((result) => normalizeResponse(result))
.catch((error) => {
metrics.recordError('third_party_error', error)
return fallbackResponse
})
}
ケース 4:関数型プログラミングでの合成
関数型スタイルでは、Promise.try() を関数合成の基本ブロックとして使うこともできます。
function pipePromise(value, ...fns) {
return fns.reduce(
(acc, fn) => acc.then((result) => Promise.try(fn, result)),
Promise.resolve(value),
)
}
const addOne = (x) => x + 1
const asyncDouble = async (x) => x * 2
function mightThrow(x) {
if (x > 10) throw new Error('Too big!')
return x
}
pipePromise(5, addOne, asyncDouble, mightThrow)
.then(console.log) // 12
.catch(console.error)
async/await との比較
Promise.try() と同等の処理は async/await でも実装できます。
// Promise.try 版
Promise.try(() => riskyOperation())
.then(handleSuccess)
.catch(handleError)
// async/await の等価版
;(async () => {
try {
const result = await riskyOperation()
return handleSuccess(result)
} catch (error) {
return handleError(error)
}
})()
シンプルなケースではどちらも置き換え可能ですが、次のような場面では Promise.try() の方が簡潔になることがあります。
- 単一の関数呼び出しだけを Promise 化したいとき
- Promise チェーンの途中に同期処理を差し込みたいとき
- 即時実行関数(IIFE)の構文ノイズを避けたいとき
ブラウザ互換性と Polyfill
Promise.try() は ES2024(ES15)で導入された標準機能です。ネイティブ実装がない環境では、次の polyfill を使えます。
if (!Promise.try) {
Promise.try = function (callback, ...args) {
return new Promise((resolve) => {
resolve(callback.apply(this, args))
})
}
}
まとめ
Promise.try() は JavaScript の Promise API に対する重要な補強であり、長年の課題だった同期/非同期エラー処理の不一致を解消してくれます。任意の関数実行結果を Promise に統一することで、次のような利点が得られます。
- エラーハンドリングの簡素化: 同期エラー捕捉のために手動で try-catch を書かなくてよい
- インターフェースの統一: コールバックが同期でも非同期でも同じ形で扱える
- 可読性の向上: チェーン構文で非同期フローが追いやすくなる
- パフォーマンス最適化: 引数を直接渡して不要なクロージャ生成を減らせる
同期例外が起こりうる非同期処理を扱う場面では、Promise.try() は実用的でエレガントな選択肢です。定型コードを減らせるだけでなく、JavaScript のエラーモデルをより一貫して予測しやすいものにしてくれます。