No promised land...

During my endeavour to create yet another Promises/A+ compliant implementation I was forced to turn the rudder by 180°.

Wait! Yet another promise implementation? Why would I do this?

I was in need of a small, self-contained exercise. The Promises/A+ specification is reasonably small and there is a full suite of mocha based compliance tests.
My actual intent was to try out Gulp as a Javascript build tool, in addition I might have learned a little more about a computational abstraction I already had enjoyed using.

Anyway. It turned out differently.
I rarely used Gulp - I had a gulpfile.js, but I was running the tests from within IntelliJ.
I didn't have time or interest to look into minification, source mapping and other things that would have been interesting with Gulp.
I rather learned a lot about promises though. More than I could have imagined.

Unfortunately I did not like everything I learned. What saddens me most is that things have made their way into ES6.

Disclaimer: The train I will be talking about has long left the station. Hence whatever I have to say probably qualifies for being a rant. I will still do my best to keep a very different tone though.

What is a promise?

You might have heard about promises before and if not there are tons of good Google hits that I don't try to compete with.
Just a very brief overview should suffice as an opener.

"Promises are about [...] providing a direct correspondence between synchronous functions and asynchronous functions." 1

A promise is an abstraction that makes asynchronous programming easier. In fact it makes asynchronous programming more like synchronous programming.

firstDo("something")  
  .then(secondDo)
  .then(thirdDo)
  .catch(function (e) {
      //handle e
});

This is the asynchronous analogue (using promises) to the code below.

try {  
    var firstResult = firstDo();
    var secondResult = secondDo(firstResult);
    var thridResult = thirdDo(secondResult);
    //...
} catch (e) {
    //handle e
}

Looks pretty familiar compared to the callback pyramid that is the alternative. And that has yet no error handling in place. Just imagine adding error callbacks right beside all the success callbacks.

firstDo("something", function(firstResult) {  
    secondDo(firstResult, function(secondResult){
        thirdDo(secondResult, function(thirdResult){
            //... 
        });
    });
});

The similarity of synchronous and asynchronous code will even increase significantly with ES6 and ES7.

async function someFunction() {  
    try {
        var firstResult = await firstDo("something");
        var secondResult = await secondDo(firstResult);
        var thirdResult = await thirdDo(secondResult);
        //...
    } catch (e) {
        // handle e
    }
}

So... what about Promises/A+?

An open standard for sound, interoperable JavaScript promises—by implementers, for implementers. 2

In principle I agree with the authors´ take on promises...

... if there just weren't these two issues.

Exception ≡ Rejection

Rejecting a promise is the asynchronous equivalent of throwing an exception. I agree, however Promises/A+ introduces one important difference between exceptions and rejected promises.
Exceptions that you don't catch get thrown to the environment, i.e. users/developers will notice. Rejected promises that you don't handle simply get lost.

Instead rejections that are not handled should be re-thrown to the environment just like exceptions are.

What about asynchronously attached handlers? We can provide a use-at-your-own-risk opt out of re-throwing like catchLater() for this case.
Another approach implemented e.g. in Q is explicit termination of promise chains via done(). This triggers a re-throw of any unhandled rejection.

var p1 = p.then(bang).catchLater();//unhandled rejections are ignored  
var p2 = p.then(bang).done();//throws reason of unhandled rejections

function bang() {  
    throw new Error("Bang!");
}

I prefer re-throwing by default with the possibility to opt out. That's as close as we can get to the behaviour of exceptions. To be fair, Promises/A+ forbids neither catchLater() nor done(). I would still expect at least recommendations from even the most minimal promise specification.

Interoperability & Assimilation

"...under the assumption that x behaves at least somewhat like a promise" 3

That makes me shiver.

Everything with a then() method has to be automagically assimilated as if it was intended to be a promise.

That's worth reiterating.

Implementers are forced to automagically assimilate anything that at least remotely looks like it could be a promise.

That overshoots the mark, and things get even worse when we look at Promises/A+ 2.3.3.3.*

  • We are obliged to hide multiple attempts to resolve or reject (2.3.3.3.3)
  • We are obliged to swallow exceptions thrown after resolve or reject was already called (2.3.3.3.4.1)

Even if it gets quite obvious that somebody else used the same fairly common English word for something unrelated, we are forced to ignore that fact and gulp the thenable anyway.

My short answer to that is: typeof p.then === 'function' ⇏ p ∈ Promise.

Don't get me wrong though.
Interoperability amongst Promises/A+ compliant implementations is a good thing.
Assimilation of non-compliant promise implementations is a good thing.

Only these should be separate operations with no magic implied. In fact Promises/A+ does not define interoperability at all. Implementers are forced to assimilate each others promises.

We would be better of if we established a marker for (compliant) promises and make non-promise (aka thenable) assimilation be explicit. We could handle foreign compliant promises as we handle our own, just add some safe guarding in order to protect from misbehaviour.
Non-promises (those that are not marked) however would have to go through an assimilation function, e.g. Promise.assimilate(thenable) or Promise.makePromise(thenable). Objects that just happen to have a function type then property could be handled as what they are - values.

Promises are legal Javascript values

The magic continues...

Promise.resolve(x).then(function(y) {  
    // y === x, unless x is of type Promise<Z> in which case y === z
})

Although Promises are legal Javascript values they are not eligible to be contained in a promise as its value. Promises/A+ requires compliant implementations to recursively unwrap thenables (2.3.3.3.1).
This appears to be a direct consequence of implicit assimilation. Arbitrary implementations of the promise pattern (not necessarily compliant with Promises/A+) could accidentally produce however deeply nested Promises by not recognising each others promises. With an implicit assimilation strategy then() is the entry point where we have to fix this up.

If we instead chose to go for explicit assimilation we would only have had nested promises inside of then() when the user creates them. For whatever reason he did, we must not touch it.

Promises in ECMAScript 2015 (aka ES6)

So far we have seen some (in my opinion) inconvenient decisions made "by implementers for implementers". Unfortunate yes, but hey I don't have to use a compliant library. I can as well choose one better fitting my requirements.

But wait. Promises/A+ was appears to having been used as blueprint for Promises in the next Javascript version. That way we have exception swallowing baked into the language. Fortunately this will be fixed by tooling some day. Chrome already tracks dangling rejected promises, i.e. Promise.reject(new Error("Boom")); leads to output to the console.

What cannot be fixed easily. then is kind of a hidden reserved word starting with ES6. I.e. unless you dive into the specification of promises you might not realize that using then as a function property is a bad idea.

Conclusion

If I wasn't clear enough about that.

Promises are a great computational abstraction for asynchronous code.
Promises/A+ did a great job in building a road towards built-in promises.

If you have asynchronous code, use promises. Just be aware of the glitches.

References: