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 functions a
and b
will work as expected, unwrapping the return value before continuing forward. As illustrated in this fiddle:
function timeout(){
var d = $.Deferred();
setTimeout(function(){ d.resolve(); },1000);
return d.promise();
}
timeout().then(function(){
document.body.innerHTML = "First";
return timeout();
}).then(function(){
document.body.innerHTML += "<br />Second";
return timeout();
}).then(function(){
document.body.innerHTML += "<br />Third";
return timeout();
});
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)
timeout().then(function(){
throw new Error("Boo");
}).then(function(){
console.log("Hello World");
},function(){
console.log("In Error Handler");
}).then(function(){
console.log("This should have run");
}).fail(function(){
console.log("But this does instead");
});
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)
timeout().then(function(){
var d = $.Deferred(); d.reject();
return d;
}).then(function(){
console.log("Hello World");
},function(){
console.log("In Error Handler");
}).then(function(){
console.log("This should have run");
}).fail(function(){
console.log("But this does instead");
});
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:
try{
throw new Error("Hello World");
} catch(e){
console.log("In Error handler");
}
console.log("This should have run");
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)
function timeout(){
var d = $.Deferred();
setTimeout(function(){ d.resolve(); },1000);
return d.promise();
}
console.log("This");
var p = timeout();
p.then(function(){
console.log("expected from an async api.");
});
console.log("is");
setTimeout(function(){
console.log("He");
p.then(function(){
console.log("?????Z???????A?????L????????G???????O???!????? *");
});
console.log("Comes");
},2000);
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.