関数の同期・非同期を気にせず処理するPromise.tryとは
Promise.tryは、同期・非同期関数を区別せず手続きを進めさせるBaseline 2025で追加されたメソッドです。同期・非同期が混在する処理に有効で、キャッシュを利用する関数などで一貫したPromiseの形で結果を扱えます。同期・非同期が混在する関数の複雑さとは別れを告げましょう。
はじめに
2025年のBaselineにPromise.try
が追加されました。Promise.try
は同期的な関数と非同期的な関数を区別せずに手続きを進めさせるメソッドです。
GitHub - tc39/proposal-promise-try: ECMAScript Proposal, specs, and reference implementation for Promise.try
ECMAScript Proposal, specs, and reference implementation for Promise.try - tc39/proposal-promise-try
github.com
この記事ではPromise.try
の使い方と、有効な場面について解説します。
Promise.tryとは
Promise.try
は引数に与えた関数の同期・非同期に関わらず、結果をPromise
に包んでから返すメソッドです。
Promise.try(callback)
.then(() => console.log('fulfilled'))
.catch(() => console.log('rejected'))
.finally(() => console.log('settled'));
Promise
で包まれた結果が渡ってくるので、非同期に書かれたasyncFn
だけではなく、syncFn
の結果もthen
を通して取り出せます。
const syncFn = () => {
return 'sync';
};
const asyncFn = async () => {
return 'async';
};
console.log(Promise.try(syncFn)); // Promise {<fulfilled>: 'sync'}
console.log(Promise.try(asyncFn)); // Promise {<pending>}
Promise.try(syncFn).then(console.log); // sync
Promise.try(asyncFn).then(console.log); // async
例外の処理もtry...catch
を利用するのではなく、catch
を通して行います。
const syncFn = () => {
throw 'sync';
};
const asyncFn = async () => {
throw 'async';
};
console.log(Promise.try(syncFn)); // Promise {<rejected>: 'sync'}
console.log(Promise.try(asyncFn)); // Promise {<pending>}
Promise.try(syncFn).catch(console.log); // sync
Promise.try(asyncFn).catch(console.log); // async
Promise.tryで引数付きの関数を呼び出す
Promise.try
を使用する際に関数へ引数を渡す方法として、直接第2引数以降に渡す方法と、新たに関数を作成して渡す方法の2つがあります。
const fn = (arg1, arg2) => {
return arg1 + arg2;
};
Promise.try(fn, 1, 2).then(console.log); // 3
Promise.try(() => fn(1, 2)).then(console.log); // 3
TypeScriptではPromise.try
の型が以下の様に定義されているので、どちらの方法でも型安全に引数を渡すことができます。
try<T, U extends unknown[]>(callbackFn: (...args: U) => T | PromiseLike<T>, ...args: U): Promise<Awaited<T>>;
Promise.tryが使えない場合
Promise.try
が使えない時代では、Promise.try
と同じことをするためにnew Promise
を用いて以下のように実装されていました。
const try = (callback, ...args) => {
return new Promise((resolve) => callback(...args));
};
記述は複雑になってしまい、知らない人からすると何をしているかが読み取りづらいです。Promise.try
があればこのような実装なしに簡単に利用できるのでとても便利です。
Promise.tryが有効な場面
Promise.try
は同期的な関数と非同期的な関数が入り乱れたものを取り扱う時に便利です。
以下の例を考えてみましょう。
const cache = new Map();
const readFileWithCache = (path): string => {
if (cache.has(path)) {
return cache.get(path);
}
return fs.readFile(path, 'utf8').then((data) => {
cache.set(path, data);
return data;
});
};
上記のreadFileWithCache
関数はpath
に存在するファイルを読み込む関数です。
初回の読み込みはfs.readFile
を用いて非同期に値を取り出しますが、2回目以降はcache
に保存された値を返します。
この関数を実行してみると、1度目の読み込みはPromise<Buffer>
が返されますが、それ以降はBuffer
を返巣ようになっています。キャッシュしてくれるのは嬉しいですが、取り出した値を利用する視点ではとても不便です。
console.log(readFileWithCache('path/to/file')); // Promise {<pending>} or Buffer
このような関数はPromise.try
を使うことで、初回と2回目以降の呼び出しで同じようにPromise<Buffer>
を返すようにできます。
console.log(Promise.try(() => readFileWithCache('path/to/file'))); // Promise {<pending>}
この例ではPromise.try
を使う以外でも使いやすい形に改良可能ではありますが、Promise.tryを活用することで、同期・非同期を意識せず統一的に扱えるメリットがよく分かるのではないでしょうか。
適切に活用することで、コードの可読性や保守性を向上させることができます。