Promise.try:
シンプルで強力な同期/非同期統合の未来
- 実装の深層とPromiseの進化
JSConf JP 2024
saku🌸 / @sakupi01
saku
Design Technologist / ex-Web Frontend @Cybozu
Goolgle Developer Expert for Web Technologies
🍏 / ☕ / 🌏 >
Promise.try:
シンプルで強力な同期/非同期統合の未来
- 実装の深層とPromiseの進化
Congrats to Promise.try🎉
先月東京で開催されたTC39 104thのMeetingで8年の時を経てStage4に!
Commit c03068b
Normative: add Promise.try (#3327)
1. JavaScriptにおける同期処理と非同期処理の
統一化の難しさ
e.g. syncOrAsyncという、同期・非同期的どちらの結果ももたらす可能性がある関数
function syncOrAsync(id) {
if (typeof id !== "number") {
throw new Error("id must be a number"); // 同期的なエラー
}
return db.getUserById(id); // 非同期的な処理
}
そのままsync/asyncな関数を扱おうとする
// Uncaught TypeError: syncOrAsync(...).then is not a function
// Property 'then' does not exist on type '"sync"'.ts(2339)
syncOrAsync()
.then(console.log)
.catch(console.error);
- 実行エラー、TypeScriptの場合はコンパイルエラー
- 同期関数である場合、
thenプロパティが存在しないため
よくやってしまいそうな方法として、
新しくPromiseを生成して、そのなかでsync/asyncな関数を解決する
new Promise((resolve) => resolve(syncOrAsync()))
.then(console.log)
.catch(console.error);
- 不必要な非同期処理は発生せず、動作としては問題ない
- 書きぶりが冗長
新しくPromiseを作成 syncOrAsync() の結果をそのまま resolve Promise を成功として完了させる syncOrAsync() が同期的な値または Promise を返す場合、その結果が .then(console.log) に渡されてコンソールに出力される もし syncOrAsync() がエラーをスローしたり、返された Promise が拒否された場合は、.catch(console.error) によってエラーがコンソールに出力される Promiseは即座に解決されるため、不必要な非同期処理は発生しない
Promise.try()が標準化される前にも、Promise.try()のような機能を提供するライブラリは存在していた
- Bluebird.js: Promise.try/Promise.attempt
- Sindre Sorhus:
p-try
「新しくPromiseを生成して、そのなかでsync/asyncな関数を解決する方法」を採用
このように、これといった書き方がなかったため、 `Promise.try()`が標準化される前にも、`Promise.try()`のような機能を提供するライブラリは存在していた
2. Promise.tryが解決すること
これらのライブラリもそうなのですが、Promise.tryが解決することは、
- 不必要な非同期処理を起こさず、同期・非同期処理を統一的に扱えるように
- 同期処理の場合も非同期処理の場合もどちらの処理でも:
- 同じようにデータを扱え
- 同じようにエラーキャッチできる
- 直感的でシンプルな構文で済む
- 同期処理の場合も非同期処理の場合もどちらの処理でも:
try {
const result = await Promise.try(syncOrAsync());
console.log(result);
} catch (error) {
console.error(error);
}
Promise.try is like a .then, but without requiring a previous Promise.
http://cryto.net/~joepie91/blog/2016/05/11/what-is-promise-try-and-why-does-it-matter/
Promise.tryは、Promise不要の.thenのようなものです。
便利!
Promise.tryを初期に推し進めていた人の記事
3. Dive into Promise.try in JSC
Promise.tryの仕様と実装を見ていきます
Promise.tryの仕様を見てみる
まずJSエンジンの実装を見る前に、Promise.tryの仕様を見る
わずか7行!

仕様自体はわずか7行で書かれていて非常にシンプルです
27.2.4.8 Promise.try ( callback, ...args )
実行された時に、以下の手順で処理を行う:
- Cをthisとする
- Cがオブジェクトでない場合はTypeErrorをスローする
- promiseCapabilityに
? NewPromiseCapability(C)の返り値を代入する - コールバック関数にargを渡して実行した結果をCompletionとして、statusに代入する
- statusがabrupt completionであれば、
promiseCapabilityのrejectにstatusのvalueを渡して実行する - そうでない場合(正常にcallbackの実行が完了した場合)は、
promiseCapabilityのresolveにstatusのvalueを渡して実行する promiseCapabilityのpromiseを返す
日本語訳するとこんな感じなのですが、
promiseCapabilityやNewPromiseCapability、Completionなど、使用を読む上で
見慣れない概念があると思うので、それを説明する。特にNewPromiseCapabilityが肝となる概念なのでそれに付随して他の概念も説明していく。
NewPromiseCapabilityと
それに関連するECMAScriptの概念
NewPromiseCapability
PromiseCapabilityRecordを生成するAbstract Operation。- 最終的には、
PromiseCapabilityRecordを含むNormal Completionを返す、またはThrow Completionする。 - JSCでの実装では
@newPromiseCapabilityに相当する
PromiseCapability Record
NewPromiseCapabilityによって生成される- Promiseを
resolve・rejectする関数とPromise自体を持ち合わせたレコード
27.2.1.1 PromiseCapability Records

Abstract Operations
- ECMAScript仕様内で共通して使用される操作をまとめたもの
- 仕様上の関数というイメージで、仕様内の他の場所から呼び出される
NewPromiseCapabilityはPromiseCapabilityRecordを生成する仕様上の関数と言える
Completion Records; ? and !
- ECMAScriptのランタイムセマンティクスの生成物に対して返されるもの
- TypeがNormal: Normal Completion。
!(感嘆符): 操作が絶対に失敗しないことを示す場合に使用
- TypeがNormal以外: Abrupt Completion。
?(疑問符): 操作が失敗する可能性があることを示す場合に使用

- ※ 仕様上はnormal completionまたはthrow completionのみが関心ごととなる
- Typeがnormalなものはnormal completionと呼ばれる。normal completion以外のCompletion Recordはabrupt completionと呼ばれる。
thorow completion以外のabruptは出てこない
組み込み関数の抽象操作を表現する過程では、関数の内部的である実装のことは気にしないため、 関数の境界を越えられないbreak/continue/returnは動作せず、 仕様レベルではnormal completionまたはthrow completionのみが関心ごととなるという意味
JSCでのPromise.tryの実装
JSCではビルトイン関数をC++で書くこともできるし、JSで書くこともできる。そして Promise.try は JS で書かれている
そんなJSで書かれたPromise.tryの実装を仕様に即して見てみましょう
function try(callback /*, ...args */)
{
"use strict";
// 2. this(通常はPromise)がオブジェクトでない場合はTypeErrorをスローする
if (!@isObject(this))
@throwTypeError("|this| is not an object");
var args = [];
for (var i = 1; i < arguments.length; i++)
@putByValDirect(args, i - 1, arguments[i]);
// 3. promiseCapabilityに? NewPromiseCapability(C)の返り値を代入する
var promiseCapability = @newPromiseCapability(this);
...
使用の一番目の手順は実装には出てこないのでスキップするとして、2番目から見ていきます
`arguments`というArrayLike を args 配列に詰め直してるだけ
...
try {
// 4. コールバック関数にargを渡して実行した結果を(Completionとして、)statusに代入する
var value = callback.@apply(@undefined, args);
// 5. エラーがスローされなければ(statusがnormal completionであれば、)
// promiseCapabilityのresolveにstatusのvalueを渡して実行する
promiseCapability.resolve.@call(@undefined, value);
} catch (error) {
// 6. エラーがスローされれば(statusがabrupt completionであれば、)
// promiseCapabilityのrejectにstatusのvalue(error)を渡して実行する
promiseCapability.reject.@call(@undefined, error);
}
// 7. promiseCapabilityのPromiseを返す
return promiseCapability.promise;
}
仕様では、Completionという概念を使ってエラーハンドリングがなされていたが、それは仕様上の概念であって、JSCのコードではJSの普通のtry-catchのエラーハンドリングとして表現されているということを補足しておく
8 Years of Journey to Stage4
Why it took so long to reach Stage 4
Why it took so long to reach Stage 4
Promise.tryは「この機能を言語仕様に追加する価値があるか」という議論に
時間を要した
Jordan Harband, Champion of the Promise.try proposal
具体的には後続スライドのような原因で時間がかかったとChampionのJordan Harbandは述べていた
Why it took so long to reach Stage 4
async/awaitの議論の進行- ES2017に入る
async/awaitの議論が進行しており、同様の機能を即時実行async関数(AIIFE)で実現できそうだった - その時点では、新しい構文を追加するほどの価値があるかどうか疑問視された
- ES2017に入る
- 需要の可視化が困難だった
- Bluebird.jsのような包括的な構成のライブラリでは、
Promise.try機能の実際の使用状況を分離して測定することが困難だった - p-tryのようなパッケージはまだ存在していなかった
- 実際のユースケースや需要を定量的に示すことが難しかった
- Bluebird.jsのような包括的な構成のライブラリでは、
使用の進捗を左右するサードパーティのライブラリの影響
- npmのダウンロード数から、Sindre Sorhusが開発したp-tryが多くの開発者に使われていたことがわかった
Promise.tryに対する実際の需要が可視化- 標準化が急速に進む
昨今のECMAScriptのPromise動向
- Cancellation in Promise
Jordanについでに「何が今後のPromiseの新機能としてアツいの?」というのを聞けたのでそれを話す
Cancellation in Promise
新たなステートであるcanceledを言語仕様に追加し、
Promiseのキャンセルをサポートする提案
というのが存在していた
cancelable-promises
およそ8年前...
- 新たなステートを追加することへのV8チームの反対
- 詳細が不明なまま、このproposalはpublic archiveとなり、Issueも閉じられる
This proposal experienced significant opposition from within Google and so I am unable to continue working on it.
2024年現在:Promiseはキャンセルできる(一応)
- cancelable-promisesのクローズ後、
AbortControllerがDOMに実装される - これを活用することでPromiseをキャンセルできるようになった
AbortControllerはDOM API
AbortControllerの課題
- ブラウザ環境に依存している
- サーバーサイドJavaScriptでの利用に制限が
ECMAScript側でキャンセル機構を標準化することで、統一的なキャンセル処理を実現することが期待されている
Cancellation in Promise の課題
- すでに標準化されたAbortControllerとの互換性を保つ方法でなければ
提案の進展は厳しい - DOMExceptionをJS側で発生させることと等しくなり、議論は難航しているよう
まとめ
Promise.tryがStage4に!JavaScriptの同期・非同期処理の統一化へ- JSCは
Promise.try()をシンプルなJavaScriptで実装している - Cancellation in Promise はJSサイドでの実装の夢を見るか?!
という今後の期待を込めて、発表を終わります
ご清聴ありがとうございました!
I hope you catch("key points") well!💡
4つの主要ブラウザのうち、2つが実装済みで、Safariにはラグがあるだけで、今回紹介するJSCでも実装済みです