Skip to content

Instantly share code, notes, and snippets.

@cowboy
Last active August 29, 2015 13:56
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cowboy/9281390 to your computer and use it in GitHub Desktop.
Save cowboy/9281390 to your computer and use it in GitHub Desktop.
Man, jQuery Deferreds annoy the fuck out of me.
// When a jQuery deferred is resolved with a single value, the arguments to
// the .then callback are the same as when .then is called directly on the
// deferred or on $.when(deferred) AND ALSO $.when(deferred, deferred).
var dfd = $.Deferred().resolve([1, 2, 3]);
dfd.then(function(a) {
console.log('[1a]', a); // [1, 2, 3]
});
$.when(dfd).then(function(a) {
console.log('[2a]', a); // [1, 2, 3]
});
$.when(dfd, dfd).then(function(a, b) {
console.log('[3a]', a, b); // [1, 2, 3] [1, 2, 3]
});
// When a jQuery deferred is resolved with multiple values, the arguments to
// the .then callback are the same as when .then is called directly on the
// deferred or on $.when(deferred) BUT NOT $.when(deferred, deferred).
var dfd = $.Deferred().resolve(1, 2, 3);
dfd.then(function(a, b, c) {
console.log('[1b]', a, b, c); // 1 2 3
});
$.when(dfd).then(function(a, b, c) {
console.log('[2b]', a, b, c); // 1 2 3
});
$.when(dfd, dfd).then(function(a, b) {
console.log('[3b]', a, b); // [1, 2, 3] [1, 2, 3]
});
// I mean, I get it. You want $.when(deferred) to be idempotent. Is that the
// right fancy word? Whatever, close enough. And spreading all those values
// across the $.when(deferred, deferred) callback would be insane. Arrays make
// sense there. But why then spread those values across the deferred.then or
// $.when(dfd) callback? Because it's magicsexy?
// Because jQuery Ajax requests are resolved with multiple values instead of
// with a single array value, the arguments to the $.when(req, req) callback
// behave very differently than the arguments to the req.then or $.when(req)
// callback. THIS REALLY CONFUSES PEOPLE. SERIOUSLY, I SEE IT ALL THE TIME.
var req = $.ajax('/');
req.then(function(resp, status, obj) {
console.log('[1c]', resp, status, obj); // response 'success' jqXHR
});
$.when(req).then(function(resp, status, obj) {
console.log('[2c]', resp, status, obj); // response 'success' jqXHR
});
$.when(req, req).then(function(a, b) {
console.log('[3c]', a, b); // [response, 'success', jqXHR] [response, 'success', jqXHR]
});
// And because the $.Deferred() factory builds new deferred objects from
// scratch without any of the internal mechanics or some kind of prototype
// exposed, it's impossible to, say, override the Deferred resolve / reject
// method with one that converts multiple values into an array before
// actually resolving / rejecting.
// Here's a "plugin" that works around this annoyance by throwing away
// the extra values: 'success' & jqXHR, just leaving the response.
$.makeAjaxResultLessShitty = String;
var req = $.ajax('/').then($.makeAjaxResultLessShitty);
req.then(function(a) {
console.log('[1d]', a); // response
});
$.when(req).then(function(a) {
console.log('[2d]', a); // response
});
$.when(req, req).then(function(a, b) {
console.log('[3d]', a, b); // response response
});
// Anyways, this all begs the question:
// WHY RESOLVE JQUERY DEFERREDS WITH MULTIPLE VALUES?
// (Arrays work pretty darned well)
@dmethvin
Copy link

dmethvin commented Mar 1, 2014

Short of having a time machine and going back to late-2010, there isn't much that can be done to change $.ajax and the way it uses $.Deferred. If you have really do have that capability, I would suggest going back and knocking off Robin Thicke. Instead we need to look at what could be done without breaking four years worth of web code.

Let's brainstorm on the design of a different interface, called $.xhr for now, that could do something different. Let's say this new interface returns a standard Promise. Here's a simple example:

$.xhr("/test/url/").then(
  function(arg) { /* resolved */ },
  function(arg) { /* rejected */ }
);

What constitutes "resolved"? Does it mean the XHR completed in some way including 404? Or does it only include successful status codes like 200? Regardless, it seems like a good idea for arg to be the XHR object and users can figure out the rest themselves so we don't need the second status arg.

What constitutes "rejected"? If it includes failures like 404, what should be passed as arg? Remember that the spec strongly advises that arg should be an Error object since any exception thrown by the code will need to be passed to the rejection handler. Should the user be expected to duck-type the arg to determine XHR vs Error? Should this be some instanceof Error object with an optional XHR property that has more info?

With $.Deferred we don't have the Error problem since exceptions are not caught/forwarded which is good for debugging (IMO) but not the same as Promise. I'll note that if your examples above were using Promise, every one of them would fail silently on any exception given Chrome's current Promise implementation.

@cowboy
Copy link
Author

cowboy commented Mar 1, 2014

The "jQuery backwards compatible API" fallacy

I understand that the decision to set jQuery users' expectations that future versions of jQuery will be "drop-in" replacements for previous versions was set a long time ago, but time and time again, major (and minor) backwards incompatible changes have shown users that this expectation is unrealistic. Users find that, despite all marketing efforts to the contrary, it's usually impractical to "upgrade" the jQuery version in a web site or app to the latest, greatest, because enough little things have changed to make it an overwhelming task.

But even after experiencing the pain of upgrading jQuery in a web site or app, do users learn this? No. They've bought into this myth, that the jQuery API is backwards compatible. Why? Because people keep propagating the "jQuery backwards compatible API" fallacy with sentences like, "Instead we need to look at what could be done without breaking four years worth of web code."

Making an API change doesn't have to break four years worth of web code because people do not need to update jQuery in their four-year-old web site or app. I mean, it's four years old already. It already works, right? So, why open up Pandora's box?

Instead of perpetuating this myth, jQuery should have long ago set users expectations far more realistically. Eg.

  • No, future versions of jQuery will NOT necessarily be backwards compatible, even though we will try to make them so.
  • No, you will probably not be able to drop-in upgrade to the latest version of jQuery in your web site or app.
  • But yes, we will maintain previous releases for a certain amount of time with critical fixes so that you don't feel like you need to upgrade to the latest, possibly-backwards-incompatible version.

(That third one is the bit that helps all this work... but even though it's a little different than what you've been doing, it's so worth it)

The ongoing argument that the jQuery API must be backwards-compatible sets unrealistic expectations. Stop it! And wait one moment... now you have carte blanche to update your API and fix four-year-old mistakes? Sweet.

@cowboy
Copy link
Author

cowboy commented Mar 1, 2014

Also, @dmethvin. Sure, resolve with the jqXHR and reject with an error object that has extra metadata expandoed onto it, like the status code and/or jqXHR object.

@louisremi
Copy link

@dmethvin's idea of a slightly different API with a better name is still a good solution to the problem. $.ajax could later be marked as deprecated and made optional in the build system.

@dmethvin
Copy link

dmethvin commented Mar 1, 2014

@cowboy, it's not just about changing old code, because I agree that the majority of old code should be left alone and in reality it is left alone.

It's about changing years of old blogs, StackOverflow advice, and documentation scattered around the net. That's why a new API is the only way to be sure that all the Google searches don't turn up a frustrating amount of incorrect information.

@dmethvin
Copy link

dmethvin commented Mar 1, 2014

Also remember we are part of an ecosystem that includes a huge number of third-party plugins. If we break some popular plugin by changing an existing API, we break the pages of people who try to write new code with that plugin and the new jQuery version. There's only so far we can go with a plugin like jQuery Migrate before it gets out of hand. Again, a new API gives us flexibility.

@cowboy
Copy link
Author

cowboy commented Mar 1, 2014

@dmethvin, I know that part of what makes jQuery maintainers good at, well, maintaining jQuery is the mindset where, when a bug or weird behavior is encountered in a browser, you fix it. But you're talking about the way the internet works; there will always be outdated or incorrect blogs, StackOverflow advice and documentation scattered around the net. It's not your job as the maintainers of a small software library to fix the way people use the internet; it's your job to make a library with an awesome API. Fortunately, just like with every other software library they use, developers will figure out how to use it. I mean, jQuery has fantastic official documentation.

I understand about the plugin thing, I happen to maintain a tool that has thousands of plugins. Updating the tool sometimes breaks plugins, which people don't like. But fortunately, users can continue to use the old version of the tool until the plugin they want to use gets updated. People aren't forced to upgrade!

@dmethvin
Copy link

dmethvin commented Mar 1, 2014

Okay, so you're proposing a drastic breaking change to the existing API. Write some code that defines the way you want it to work. Explain why we should make that breaking change rather than creating a new API that uses the emerging Promise standard.

@cowboy
Copy link
Author

cowboy commented Mar 1, 2014

A new API that uses the emerging Promise standard sounds fantastic!

Also, I think that setting users' expectations in the ways I've suggested sounds fantastic, too.

Win/win!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment