Promise.try: Deep Dive into Implementation and the Evolution of Promises

JSConf JP 2024
saku🌸 / @sakupi01

saku

Web Frontend Engineer
@Cybozu

🍏/ ☕/ 🌏 > ❤️
𝕏 @sakupi01

Promise.try: Deep Dive into Implementation and the Evolution of Promises

Available in JA

Congrats to Promise.try🎉

After 8 years, it reached Stage 4 at the TC39 104th Meeting held in Tokyo last month!

Commit c03068b
Normative: add Promise.try (#3327)

1. The Difficulty of Unifying Synchronous and Asynchronous Processing in JavaScript

e.g. A function called syncOrAsync that can yield either synchronous or asynchronous results

function syncOrAsync(id) {
    if (typeof id !== "number") {
        throw new Error("id must be a number"); // Synchronous error
        }
    return db.getUserById(id); // Asynchronous processing
}

Method ①: Attempt to handle sync/async functions as they are

// 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);
  • Execution error, compile error in TypeScript
  • If it's a synchronous function, the then property does not exist

Method ②: Use Promise.resolve() as is

try {
    Promise.resolve(syncOrAsync())
        .then(console.log)
          .catch(console.error); // Catch asynchronous errors
} catch (error) {
    // Catch synchronous errors
    console.error(error);
}
  • If a synchronous error occurs, it cannot be caught with catch
  • Need to use try-catch to catch it

Method ③: Handle sync/async functions within Promise.resolve().then()

Promise.resolve().then(syncOrAsync)
    .then(console.log)
      .catch(console.error);
  • Simple writing style
  • Even synchronous functions are executed asynchronously → unnecessarily asynchronous

Method ④: Create a new Promise and resolve sync/async functions within it

new Promise((resolve) => resolve(syncOrAsync()))
    .then(console.log)
      .catch(console.error);
  • Unnecessary asynchronous processing does not occur
  • Writing style is verbose

Before Promise.try was standardized, libraries offering similar functionality existed

Adopted the method of "creating a new Promise and resolving sync/async functions within it" as in method ④

2. What Promise.try Solves

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 is like a .then, but without requiring a previous Promise.

  • Allows unified handling of synchronous and asynchronous processing without unnecessary asynchronous processing
  • Whether synchronous or asynchronous processing:
    • Can handle data in the same way
    • Can catch errors in the same way
  • Intuitive and simple syntax
Promise.try(syncOrAsync())
 .then(console.log)
   .catch(console.error);

Convenient!

3. Dive into Promise.try in JSC

Let's Look at the Specification of Promise.try

spec

spec-marked

NewPromiseCapability

  • Abstract Operation that generates a PromiseCapabilityRecord.
  • Ultimately returns a Normal Completion containing a PromiseCapabilityRecord, or throws a Completion.
  • In JSC, it corresponds to @newPromiseCapability

27.2.1.5 NewPromiseCapability ( C )

PromiseCapability Record

PromiseCapabilityRecord

Abstract Operations

  • A collection of operations commonly used within the ECMAScript specification
  • Conceptual functions within the specification that are called from other parts of the specification

5.2.1 Abstract Operations

Re: NewPromiseCapability

  • Abstract Operation that generates a PromiseCapabilityRecord.
  • Ultimately returns a Normal Completion containing a PromiseCapabilityRecord, or throws a Completion.
  • In JSC, it corresponds to @newPromiseCapability

27.2.1.5 NewPromiseCapability ( C )

Completion Records; ? and !

  • Returned to the runtime semantics of ECMAScript
  • If the type is Normal: Normal Completion.
    • ! (exclamation mark): Used to indicate that the operation will never fail
  • If the type is not Normal: Abrupt Completion.
    • ? (question mark): Used to indicate that the operation may fail

CompletionRecords

JSCでのPromise.tryの実装

// 1. Accepts a callback and a list of arguments (arguments) to pass to it
function try(callback /*, ...args */)
{
    "use strict";
    // 2. If this (usually Promise) is not an object, throw a 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. Generate a PromiseCapability using C (this) with newPromiseCapability
    var promiseCapability = @newPromiseCapability(this);
...

[JSC] Implement Promise.try #24802

JSCでのPromise.tryの実装

...
    try {
        // 4. Execute the callback with the argument list (arguments)
        var value = callback.@apply(@undefined, args);
        // 6. If status is Normal (no errors), pass the execution result to promiseCapability's resolve
        promiseCapability.resolve.@call(@undefined, value);
    } catch (error) {
        // 5. If status is Abrupt (not Normal, with errors), pass the execution result to promiseCapability's reject
        promiseCapability.reject.@call(@undefined, error);
    }
    // 7. Return promiseCapability's Promise
    return promiseCapability.promise;
}

8 Years of Journey to Stage4

Why it took so long to reach Stage 4

Why it took so long to reach Stage 4


jordan-promise

Promise.try took time to discuss whether "this feature is worth adding to the language specification"

Why it took so long to reach Stage 4

  1. Progress of async/await discussions
    1. Discussions on async/await to be included in ES2017 were progressing, and similar functionality seemed achievable with immediately-invoked async functions (AIIFE)
    2. At that time, there was skepticism about whether adding new syntax was worth it
  2. Difficulty in visualizing demand
    1. In comprehensive libraries like Bluebird.js, it was difficult to isolate and measure the actual usage of the Promise.try feature
    2. Packages like p-try did not yet exist
    3. It was challenging to quantitatively demonstrate actual use cases and demand

Influence of Third-Party Libraries on Usage Progress

  • p-try emerged, and it became apparent that it recorded a large number of downloads
  • Actual demand for Promise.try became visible
  • Standardization progressed rapidly

Cancellation in Promise

A proposal to support Promise cancellation by adding a new state, canceled, to the language specification

promise-cancel

Such a proposal existed

Cancellation in Promise

About 8 years ago...

  • Opposition from the V8 team to adding a new state
  • Without further details, this proposal became a public archive, and the Issue was closed

This proposal experienced significant opposition from within Google and so I am unable to continue working on it.

Why was this proposal withdrawn? #70

As of 2024: Promises Can Be Canceled (Sort of)

  • After the closure of cancelable-promises, AbortController was implemented in the DOM
  • By utilizing this, Promises can be canceled
  • AbortController is a DOM API

Challenges of AbortController

  • Dependent on the browser environment
  • Limited use in server-side JavaScript

There is an expectation to standardize the cancellation mechanism on the ECMAScript side to achieve unified cancellation processing

Challenges of Cancellation in Promise

  • Unless it is compatible with the already standardized AbortController, progress on the proposal is difficult
  • It becomes equivalent to generating a DOMException on the JS side, and discussions seem to be stalling

Summary

  • Promise.try() reached Stage 4! Towards the unification of synchronous and asynchronous processing in JavaScript
  • JSC implements Promise.try() in an ECMAScript-way
  • Does Cancellation in Promise dream of implementation on the JS side?!

Thank you for listening!

I hope you catch("key points") well!💡

8 years ago, I was in junior high or high school

Of the four major browsers, two have implemented it, and Safari is just lagging behind, but it is already implemented in JSC, which I will introduce today

Since synchronous functions are not thenable, to .then(), you must create a Promise with Promise.resolve() and then execute it with .then()

Create a new Promise Resolve the result of syncOrAsync() as is Complete the Promise as successful If syncOrAsync() returns a synchronous value or a Promise, the result is passed to .then(console.log) and output to the console If syncOrAsync() throws an error or the returned Promise is rejected, the error is output to the console by .catch(console.error) The Promise is resolved immediately, so unnecessary asynchronous processing does not occur

As you can see, handling synchronous and asynchronous processing uniformly in JavaScript has been a difficult problem, requiring verbose writing styles or unnecessary event loops.

How did existing libraries achieve Promise.try in JavaScript?

An article by someone who initially pushed for Promise.try

Before looking at the JS engine implementation, let's look at the ECMAScript specification for Promise.try, which serves as its blueprint

JSCではビルトイン関数をC++で書くこともできるし、JSで書くこともできる。そして Promise.try は JS で書かれている

`arguments`というArrayLike を args 配列に詰め直してるだけ

仕様では、Completionという概念を使ってエラーハンドリングがなされていたが、JSCのコードではJSの普通のtry-catchのエラーハンドリングとして実現されている

Completionみたいな概念は現れていないことを補足する