@Domenic has a very thorough article on the failings of jQuery deferred objects: You're missing the Point of Promises. In it Domenic highlights a few failings of jQuery promises in comparison to others including Q, when.js, RSVP.js and ES6 promises.
I walk away from Domenic's article feeling that jQuery promises have an inherent failing, conceptually. I am trying to put examples to the concept.
I gather there are two concerns with the jQuery implementation:
1. The .then
method is not chainable
In other words
promise.then(a).then(b)
jQuery will call a
then b
when the promise
is fulfilled.
Since .then
returns a new promise in the other promise libraries, their equivalent would be:
promise.then(a)
promise.then(b)
2. The exception handling is bubbled in jQuery.
The other issue would seem to be exception handling, namely:
try {
promise.then(a)
} catch (e) {
}
The equivalent in Q would be:
try {
promise.then(a).done()
} catch (e) {
// .done() re-throws any exceptions from a
}
In jQuery the exception throws and bubbles when a
fails to the catch block. In the other promises any exception in a
would be carried through to the .done
or .catch
or other async catch. If none of the promise API calls catch the exception it disappears (hence the Q best-practice of e.g. using .done
to release any unhandled exceptions).
Do the problems above cover the concerns with the jQuery implementation of promises, or have I misunderstood or missed issues?
Edit This question relates to jQuery < 3.0; as of jQuery 3.0 alpha jQuery is Promises/A+ compliant.
Update: jQuery 3.0 has fixed the problems outlined below. It is truly Promises/A+ compliant.
Yes, jQuery promises have serious and inherent problems.
That said, since the article was written jQuery made significant efforts to be more Promises/Aplus complaint and they now have a .then method that chains.
So even in jQuery
returnsPromise().then(a).then(b)
for promise returning functionsa
andb
will work as expected, unwrapping the return value before continuing forward. As illustrated in this fiddle:However, the two huge problems with jQuery are error handling and unexpected execution order.
Error handling
There is no way to mark a jQuery promise that rejected as "Handled", even if you resolve it, unlike catch. This makes rejections in jQuery inherently broken and very hard to use, nothing like synchronous
try/catch
.Can you guess what logs here? (fiddle)
If you guessed
"uncaught Error: boo"
you were correct. jQuery promises are not throw safe. They will not let you handle any thrown errors unlike Promises/Aplus promises. What about reject safety? (fiddle)The following logs
"In Error Handler" "But this does instead"
- there is no way to handle a jQuery promise rejection at all. This is unlike the flow you'd expect:Which is the flow you get with Promises/A+ libraries like Bluebird and Q, and what you'd expect for usefulness. This is huge and throw safety is a big selling point for promises. Here is Bluebird acting correctly in this case.
Execution order
jQuery will execute the passed function immediately rather than deferring it if the underlying promise already resolved, so code will behave differently depending on whether the promise we're attaching a handler to rejected already resolved. This is effectively releasing Zalgo and can cause some of the most painful bugs. This creates some of the hardest to debug bugs.
If we look at the following code: (fiddle)
We can observe that oh so dangerous behavior, the
setTimeout
waits for the original timeout to end, so jQuery switches its execution order because... who likes deterministic APIs that don't cause stack overflows? This is why the Promises/A+ specification requires that promises are always deferred to the next execution of the event loop.Side note
Worth mentioning that newer and stronger promise libraries like Bluebird (and experimentally When) do not require
.done
at the end of the chain like Q does since they figure out unhandled rejections themselves, they're also much much faster than jQuery promises or Q promises.