Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Problems inherent to jQuery $.Deferred (jQuery 1.x/2.x)

@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.

like image 448
Brian M. Hunt Avatar asked May 19 '14 18:05

Brian M. Hunt


People also ask

What is deferred function in JQuery?

Deferred() method in JQuery is a function which returns the utility object with methods which can register multiple callbacks to queues. It calls the callback queues, and relay the success or failure state of any synchronous or asynchronous function.

What is JQuery deferred and promise object?

A promise is, as the name says, a promise about a future value. Together with the Promise object , Deferred represents the jQuery implementation of promises. You can attach callbacks to it to get that value. It possesses a subset of the methods of the Deferred object : always(), done(), fail(), state(), and then().

What is deferred and promise Javascript?

The deferred. promise() method allows an asynchronous function to prevent other code from interfering with the progress or status of its internal request.


1 Answers

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.

like image 114
Benjamin Gruenbaum Avatar answered Sep 29 '22 05:09

Benjamin Gruenbaum