Introduction of promise.return() and promise.throw() similar to generator.return() and generator.throw()

# Igor Baklan (7 years ago)

Assuming analogy in tuples (function*, yield, Generator.prototype.return(), Generator.prototype.throw() ) and (async function, await, ?Promise.prototype.return(), ?Promise.prototype.throw()), it would be good to have Promise.prototype.return() and Promise.prototype.throw() which in case of async/await function should just "inject" some return/throw action in place of pending await statement.

While for .then-like promises it should just force promise to abnormal completion (with provided result) without calling callbacks provided in .then/.catch methods (if they was not yet called before, at the time of "abruption" attempt).

In case of general new Promise(promiseExecutor)-like promises it supposedly should work in following manner: some abnormalReturn / abnormalThrow signals should be sent to promiseExecutor, then promiseExecutor can handle this signal and initialte "preventDefault behavior" which will correspond to catch/finally blocks capabilities, or promiseExecutor can ignore and not handle this signals, then default behavior should be invoked (which will lead to promise abnormal completion with value provided in Promise.prototype.return(...)/Promise.prototype.throw() call) - this second option will correspond to that case when assumptive meta-code of promiseExecutor does not contain imaginary catch/finally blocks.

In code that last case might look like this:

const waitAsync = (delay) => (new Promise(
  (setOk, setErr, signalsSource) => {
    const beforeComplete = () => {clearTimeout(tid);}
    const tid = setTimeout(
      () => {beforeComplete(); setOk();},
      delay
    );
    signalsSource.on(
      "abnormalReturn", (abnormalReturnSignal) => {
        if (abnormalReturnSignal.value < 5) {
          // reject offered for abnormal rerun value when it < 5
          abnormalReturnSignal.preventDefault();
          return;
        }
        // do some finally-like cleanup assuming that 
        // abnormalReturnSignal default behavior will be launched
        beforeComplete();
    );
    signalsSource.on(
      "abnormalThrow", (abnormalThrowSignal) => {
        if (abnormalThrowSignal.value instanceof HatedError) {
          // catch and reject HatedError-es, corresponds to catching this one 
          // and re-throwing/propagating anything else by abnormalThrowSignal default behavior
          abnormalThrowSignal.preventDefault();
          return;
        }
        // do some finally-like cleanup assuming that 
        // abnormalThrowSignal default behavior will be launched
        beforeComplete();
    );
  }
));

// waitTask = waitAsync(100);
// waitTask.then((value) => {console.log(`wait ok, value = ${value}`)});
// waitTask.return(3); - abnormal return should be rejected by promice executor logic, since 3 < 5
// waitTask.return(103); - abnormal return should happen since !(103 < 5) and default behavior should work

//   "wait ok, value = 100" message should be printed, without delay in 100ms

In case if some transpiler decide to implement async functions using generator functions this promise.return(...) and promise.throw(...) functions can be obtained almost out-of-the-box using Generator.prototype.return() and Generator.prototype.throw().

Approximately it might look like:

var someFunctionToTranslate = function (...args) {
   // ... some code here ...
   var someValue = await somePromice;
   // ... some code here ...
};

might be translated to

var someFunctionToTranslate = _rawGeneratorFuncToNormalAsyncLikeFunc(function* (...args) {
   // ... some translated code here ...
   var _tmpAwaitTask = new AwaitTask(somePromice);
   yield _tmpAwaitTask;
   var someValue = _tmpAwaitTask.awaitResult;
   // ... some translated code here ...
});

Then promice.return(...) on obtained promise should cause generator.return(...) on underling generator object, and lead to abnormal completion delivered through yield instruction which here represents a part of await statement translation.

By the way during my reading of Generator#return() and Generator#throw() after reading bluebirdjs-cancellation I've suddenly aware that native return statement can be "completely rejected" by the means of finally block, meaning that in general for example there are no 100%-guaranties that Generator#return() will force generator to stop iterating same as there are either no guaranties that Generator#throw() will force generator to stop (but this one is more obvious regarding catch statement natural behavior).

Here is some patched example from Generator#return()

function* gen() { 
  // shielded yield 1
  do { try { yield 1; } finally{ continue; } } while (false);
  // shielded yield 2
  do { try { yield 2; } finally{ continue; } } while (false);
  // shielded yield 3
  do { try { yield 3; } finally{ continue; } } while (false);
}

var g = gen();

console.log(g.next());        // { value: 1, done: false }
console.log(g.return("foo")); // { value: 2, done: false } // impudent generator rejects return
console.log(g.return("bar")); // { value: 3, done: false } // impudent generator rejects return
console.log(g.next());        // { value: undefined, done: true } // generator finishes by its own will

So considering this possibility, I would say, in case if promise.return() would be implemented, provide possibility to promiseExecutor to intercept and override .return() action is quit natural (assuming that by default it will not handle that signals and not preventDefault on it, and that means - in general work like regular code without finally/strange-finally blocks)

# Isiah Meadows (7 years ago)

Look up the "revealing constructor pattern". There's a reason why only the creator can resolve a promise imperatively.

# Igor Baklan (7 years ago)

Yes, it's good note, that it can be "too public" and may be some one would like to prevent external "intrusion" into it's "private state", but it can be easily solved by the means of a wrappers, that wraps some private target and delegate to it only "safe calls", that will not interrupt-it/cancel-it, it can look like privateSattePromise.preventAbnormalCompletion() - which returns some wrapped promise with more "safe" behavior, then this promise can be shared with "wider audience". But from the opposite side - if somebody make some internal logic, and do not intend to return that internal objects "outside", why he should complicate his life so much (and do not "trust himself" if both methods - which produces some promise and consumes that promise - are private in some place)? As an example of such kind of wrappers I can offer you java(Collections#unmodifiableList). It's conception is very simple - it takes some mutable list and wraps it by some wrapper which prevents modification, as a result you'll obtain some "semi-immutable" list, meaning that you will not be able modify it through that wrapped instance, but still can do some changes over initial collection, and that changes will be quite visible through the wrapped one.

So from my point of view, it can be 2-wo approaches - always expose that "external intrusion api", and the explicitly "hide" that api by the means of some promise.giveMeSafeWrapper()-like api. Or an other option, just do not expose that api by default, and say that if you want to create that "extra public" promise you should use some subclass of regular promise, like extraPublicPromise = new Promice.ExtraPublic(executor), and then ofcourse, it would be more explicit, which stuff is internal, and should be wrapped before giving it somewhere outside, and knowing that all the rest is "safe" in terms of "external intrusion".

But denying that capability at all, being to lazy for writing .giveMeSafeWrapper()-like calls is also not very good choice (as for me). (By the way, approach that was described in the-revealing-constructor-pattern#historical-origins doesn't looks "so insane". Considering deferred = Q.defer(); and p = deferred.promise;, I see that there are some public part and private part of that object. And if they would for example share most of api on both of them, but private instance has "alittle bit more" available api for control, then in would be not so bad idea. Again, assuming that using private instance for task completion should NOT be "normal api", but as some sort of "abnormal completion api" it can be quite useful).

# James Browning (7 years ago)

If you really need an object that you describe then you use a function like this to create them:

function deferred(executor=() => {}) {
    let _resolve
    let _reject
    let promise = new Promise((resolve, reject) => {
        _resolve = resolve
        _reject = reject
    })
    promise.return = _resolve
    promise.throw = _reject
    return promise
}

and you can use it as such:

const p = deferred(resolve => setTimeout(resolve, 1000, "Hello, world!")

p.then(result => console.log(result)) // Will display Interrupt not

Hello, world!
// resolve early, no need for preventDefault as only first call to
resolve is considered
p.return("Interrupted")

Something to be aware of is generator return/throw is not about Promises, its about sending values to and from a generator to allow two way communication with a execution engine (e.g. creating applications which can be ported to different environments)

function* app() {
    yield ['changeBackground', 'green']
    yield ['wait', 1000 /* milliseconds */]
    yield ['changeBackground', 'aquamarine' /* missplet */]
}

function engine() {
    const iter = app()
    function step() {
        const { value, done } = app.next()
        if (done) {
            return // Nothing left to do
        }
        else {
            if (value[0] === 'changeBackground') {
                if (!colors.includes(value[1])) {
                    iter.throw(new Error("Color not found"))
                }
                // ...etc
       }
}

In generators you can respond to values, errors, and returns created from the outside, even try-finally isn't something special for generators, regular functions can skip return with try-finally too e.g.:

function foo() {
    try {
        return "Hello"
    } finally {
        return "Goodbye"
    }
}

console.log(foo()) // Goodbye because return "Hello" was overridden by finally

Promises aren't generators, async functions aren't either, although a polyfill can use generators to implement correct async functions by replacing await with yield and wrapping them with a spawn function, however thats a polyfill detail, and the polyfill almost certainly won't replace await foo with yield AwaitResult(foo) because that's trivially hand-able with promises themselves (see the officially suggested polyfill: tc39.github.io/ecmascript-asyncawait/#desugaring).

Generally you won't need anything like Q.deferred, you can just transfer the resolve/reject functions to wherever you need them using either closures or objects.

# Igor Baklan (7 years ago)

Actually initial discussion which "inspires" me on this thoughts was in cancellation-tokens-related topic. So here I didn't mean that it is impossible to implement that kind of functionality on "home made" promises. I mean that it would be good to have such functionality to "cancel"/"interrupt" execution flow of natively generated promises, in particular to do some "cancellation" of async task represented by async function or "unsubscribe" callback in construction consumer = provider.then(treatSomehow); and then write consumer.throw(new CancelationError("some reason")) instead of writing consumer.cancel() which might be "controversial" (whether we intend to cancel provider too, in which form we would like to do that (on which "rail" that cancellation should be delivered? on success-rail, on failure-rail or on some other extra-toxic cancellation-rail)). Ofcourse it can be some very raw tools to do cancellation, but if they were present, any "home made cancellation" would be much more doable and easy to implement then it is now.

Regarding finally/return relationship I didn't mean that it is some special feature of generators, instead, ofcourse I first of all check it on regular functions (not only on js but in java too), and for me it was more remarkable that return can be "replaced" with break or continue by the means of finally block which is more interesting case then replacing return with trow or vise versa (which ofcourse also may lead to rather "strange" behavior and rare developer can guess that and/or imagine that strange situation before someone else told him about it). So putting that example with implicit return <--> continue replacement I didn't mean to show some "extra" behavior of generator.return() I only would like to highlight that function* generators implementation of .return is QUIT VALID and really simulates return behavior in place of yield instruction (without omitting any strange "outcome" on execution flow which finally block may produce). And it is actually one of key differences with how bluebirdjs cancleation works as follows from bluebirdjs-cancellation. There is assumed that cancellation action cause that "only active finally blocks (finally callbacks) are executed" and then cancellation completes (there are no chance to "override" cancellation propagation from finally callbacks in that api) while in "real code" it is possible (and in generators to ...).

By the way in flavor of Promise.resolve and Promise.reject I would rather "rename" Q.deferred into Promise.never, in fact it would be almost the same - factory of some promises that never completes it self by its own but rather wait until someone pushes it from outside by the means of abnormal .return or .throw. It looks maybe little bit strange, but describes the thing nicely :)