k8o

関数の同期・非同期を気にせず処理するPromise.tryとは

公開: 2025年3月8日(土)
更新: 2025年3月28日(金)
閲覧数48 views

Promise.tryは、同期・非同期関数を区別せず手続きを進めさせるBaseline 2025で追加されたメソッドです。同期・非同期が混在する処理に有効で、キャッシュを利用する関数などで一貫したPromiseの形で結果を扱えます。同期・非同期が混在する関数の複雑さとは別れを告げましょう。

JavaScriptBaseline 2025PromisePromise.try

はじめに

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を活用することで、同期・非同期を意識せず統一的に扱えるメリットがよく分かるのではないでしょうか。 適切に活用することで、コードの可読性や保守性を向上させることができます。