tc39 / proposal-async-await

Async/await for ECMAScript
https://tc39.es/proposal-async-await/
Apache License 2.0
1.58k stars 106 forks source link

Proposal: Adapt return value to different Promise implementations #40

Closed dfahlander closed 9 years ago

dfahlander commented 9 years ago

It would be a good thing if async functions would return the same type of promise that was awaited on in their function bodies. This would align with how typescript is implementing it (according to this issue). The async/await keywords could then be used together with various promise implementations without loosing the bells and whistles that comes with various Promise libraries such as bluebird, Q, WinJS and Dexie.

External Promise implementations can have features that would be lost if converting them to the 'standard' Promise as suggested in the readme of this repository.

Therefore, I would propose a more adaptive implemetation of spawn() that can accept and forward any A+ compatible Promise that was awaited for:

function spawn(genF, self) {
    var gen = genF.call(self);
    function step(nextF,initial) {
        var next = nextF();
        if (next.done) {
            // finished with success, resolve the promise
            return initial ?

                // Nothing was awaited. Direct return value must be converted
                // to promise:
                Promise.resolve(next.value) :

                // Return the value as is and let the promise
                // implementation take care of the A+ compatible promise chain
                next.value;
        }
        // not finished, chain off the yielded promise and `step` again
        if (!next.value || typeof next.value.then !== 'function')
            return step(function(){
                // Don't accept awaiting a non-promise such as "await 3;".
                // By not accepting that, we could detect bugs better.
                return gen.throw(new TypeError("Only acceptable to await a Thenable"));
            }, initial);
        return next.value.then(function (v) {
            return step(function () { return gen.next(v); });
        }, function (e) {
            return step(function () { return gen.throw(e); });
        });
    }

    try {
        return step(function() { return gen.next(undefined); }, true);
    } catch (e) {
        // Failure before code has yielded any Promise.
        return Promise.reject(e);
    }
}

Specifically, for my library, Dexie.js, there is a feature that enables 'Promise-Specific Data' which is analogue to 'thread static data' for promises. The entire Dexie API depends on that feature because it enables things like reentrant mutexes, implicit transaction scope, sub transaction awareness and some more API sugar. I believe that bluebird, Q, WinJS and others also would benefit from having this openess in how async/await behave.

By not binding the async keyword to a specific implementation of Promise, we would allow new Promise implementations with features that we cannot think of today and still use them with async/await.

sebmck commented 9 years ago

External Promise implementations can have features that would be lost if converting them to the 'standard' Promise as suggested in the readme of this repository.

For reference, where in the README is this mentioned?

dfahlander commented 9 years ago

It's how I interpret the behaviour when applying the rules from Rewrite and Spawning.

sebmck commented 9 years ago

Those sections are only informative and not totally representative of an implementation.

dfahlander commented 9 years ago

Ok :+1:

Do you know if it has been discussed require this kind of adaptivity for async?

zloirock commented 9 years ago

@dfahlander compositional functions? Although not exactly.

dfahlander commented 9 years ago

Ok, that's an even broader approach. Is it still an open issue on how to accomplish this? Can it be done without having to configure the behavior of async/await? Or would it be acceptable to have a least common denominator, such as an interface that all asyncronic primitives would have to implement to work with async/await?

dfahlander commented 9 years ago

Ok, sorry. I saw that compositional functions use different keywords for async. That's fine. What I am interested in is a fine tuning of the async/await spawning method.

bterlson commented 9 years ago

This is an interesting idea but I'm not sure how well it would work in practice. I think this is morally equivalent to passing through promises returned from then handlers, and has the same issue. Namely, if you think about the following code:

async function get(url) {
    if(cache[url]) return cache[url];  // no promise returned, so wrapped in built-in promise
    let data = await $.get(url);
    if(!url.endsWith('.json')) return data; // jQuery promise returned
    return await handleJSON(data); // whatever kind of promise handleJSON returns
}

get('foo.json').? // what can I call on this promise?

Depending on how you call get, you get a different promise. Today libraries use many different promise libraries so this problem will be quite pervasive. Worse, it seems like it will be very difficult to debug/work around when you need to. Thoughts?

domenic commented 9 years ago

Yeah it seems better if you want a specific kind of promise to do something like

function bb(fn) {
  return (...args) => BluebirdPromise.resolve(fn(...args));
}

const get = bb(async url => {
  if (cache) ...
});

otherwise you just get the standard Promise.

dfahlander commented 9 years ago

@bterlson Yes, I've been thinking about that and it's a problem unless the function author understands the rules of how async would work. If the author of the async function want to ensure a single Promise type to return, (s)he could start with an await expression that returns the desired promise type to unify the rest of the function.

async function get(url) {
    await CustomPromise.resolve(); // This line will unify all promises below to CustomPromise
    if(cache[url]) return cache[url];
    let data = await $.get(url);
    if(!url.endsWith('.json')) return data;
    return await handleJSON(data);
}

Note that this is only required when you are mixing promise implementations or returning something directly in the same function. In most cases people will likely stick to a single Promise implementation in a same function.

dfahlander commented 9 years ago

@domenic Your suggestion might work for some use cases. But at least in my use case, the internal steps between the await calls must be done using Dexie.Promise. Otherwise I would loose the indexedDB transaction between the await calls due to an incompatibility between indexedDB and most ES6 Promise implementations. I would also loose the Promise-specific data feature between the calls.

domenic commented 9 years ago

I don't think you should be using async/await if you need implementation-specific features. It's not a general method for triggering .then calls; it's a way of doing standard promises (which can be hopefully optimized even better given the guarantees standard promises make).

dfahlander commented 9 years ago

It is a better design to be dependent on an interface rather than on an implementation. Especially for a built-in keyword.

dfahlander commented 9 years ago

It's not unlikely that more innovative Promise features will be developed in libraries. Like Dexie has its PSD, other libs has cancellation tokens and God knows what will show up out there...

Also, we have the Task and Observable primitives trying to be addressed by Compositional functions.

What about this: Instead of expecting a Thenable (as I suggested), require an Awaitable interface more simplistic and maybe more optimizable than Thenable. It's use case is only to be called from the async engine.

interface Awaitable {
    await(onSuccess: Function, onReject: Function): Awaitable;
}

Awaitable.await would be simpler than Promise.then: It only needs to supports a single listener onto each instance.

Awaitable.await would be compatible with Promise.then. To support it in standard Promise, do Promise.prototype.await = Promise.prototype.then.

I think that since async/await is such a great improvement of the language, all libraries would directly support the Awaitable interface including Task and Observable.

It wouldn't be an issue if all libraries out there, including non-promise asyncronic primitives. would have to add 'await' function to their prototype in order to work with async/await. This would not only solve the issue I have in Dexie.Promise but also Compositional functions without introducing new keywords like 'task' and 'observable'.

domenic commented 9 years ago

This proposal is not a general method for triggering .await calls; it's a way of doing standard promises (which can be hopefully optimized even better given the guarantees standard promises make).

dfahlander commented 9 years ago

I still believe it is better to be dependent on an interface rather than on an implementation. Especially for a built-in keyword.

bterlson commented 9 years ago

I don't think we will pursue this for now. Support for promise subclasses is a good argument for composition functions. For async functions it makes more sense to use the same behavior as promises WRT subsuming other promise types. If you want a "FooPromise" out of an async function you could also convert with var p = FooPromise.resolve(asyncFunction()). It would also probably be easy with decorated async functions.

dfahlander commented 9 years ago

@bterlson - the issue with FooPromise.resolve(asyncFunction()) is that features will get lost between the awaits in the async function.

As a result, these promise implementations will not be able to use async/await.

bterlson commented 9 years ago

It's a good point that when using async functions to await non-native promise-returning functions will result in the non-native promises being subsumed by native promises. You suggest a special case for libraries that have the same signature for .then, which might help (at some cost) but doesn't address libraries that have special conventions for, for example, propagating the cancellation signal, on progress handlers, etc.

In general I believe that developers should know exactly the kind of promise they're going to get from an async function (whether its because async functions always return native promises or because the async function is tagged in some way to indicate its return type ala composition functions). I don't think it's acceptable to let callees of an async function decide what type of promise the function returns. This seems like asking for subtle bugs and breakage when libraries change.

giancarloa commented 8 years ago

@dfahlander ... i agree with you... we use $q with some monkey patching that allows us to capture "unhandled rejections", that is, promises that are rejected but don't have a rejected handler which we consider to be unhandled exception... anyway... i for this and other reasons would like to use our promises from async functions... i am having a hard time understanding the reasoning behind having to use the global Promise... anyway... thanks for your attempt..