Skip to main content

Bug Tracker

Side navigation

#11010 closed enhancement (fixed)

Opened December 13, 2011 03:38PM UTC

Closed June 18, 2012 03:30PM UTC

Last modified June 18, 2012 03:58PM UTC

Make Deferred.then == Deferred.pipe like Promise/A

Reported by: jaubourg Owned by:
Priority: low Milestone: 1.8
Component: deferred Version: 1.7.1
Keywords: 1.8-discuss Cc:
Blocked by: Blocking:
Description

It consists primarily in having promise.then implemented like promise.pipe.

We can keep current promise.then under a new alias (attach?) for people who would still want to add callbacks without the cost promise.pipe implies (though we have done, fail and progress for this already).

Change is breaking but only to those who used the value returned by promise.then (either by storing the object or using chaining). I think the tradeof is reasonable enough.

Attachments (0)
Change History (19)

Changed December 13, 2011 03:38PM UTC by jaubourg comment:1

component: unfileddeferred
keywords: → 1.8-discuss
milestone: None1.8
priority: undecidedlow

Changed December 13, 2011 03:40PM UTC by jaubourg comment:2

description: It consists primarily in having promise.then implemented like promise.pipe. \ \ We can keep current current promise.then under a new alias (attach?) for people who would still want to add callbacks without the cost promise.pipe implies. \ \ Change is breaking but only to those who used the value returned by promise.then (either by storing the object or using chaining). I think the tradeof is reasonable enough.It consists primarily in having promise.then implemented like promise.pipe. \ \ We can keep current promise.then under a new alias (attach?) for people who would still want to add callbacks without the cost promise.pipe implies (though we have done, fail and progress for this already). \ \ Change is breaking but only to those who used the value returned by promise.then (either by storing the object or using chaining). I think the tradeof is reasonable enough.

Changed December 13, 2011 04:29PM UTC by jaubourg comment:3

description: It consists primarily in having promise.then implemented like promise.pipe. \ \ We can keep current promise.then under a new alias (attach?) for people who would still want to add callbacks without the cost promise.pipe implies (though we have done, fail and progress for this already). \ \ Change is breaking but only to those who used the value returned by promise.then (either by storing the object or using chaining). I think the tradeof is reasonable enough.It consists primarily in having promise.then implemented like promise.pipe.\ \ We can keep current promise.then under a new alias (attach?) for people who would still want to add callbacks without the cost promise.pipe implies (though we have done, fail and progress for this already).\ \ Change is breaking but only to those who used the value returned by promise.then (either by storing the object or using chaining). I think the tradeof is reasonable enough.

+1

Changed December 14, 2011 03:34AM UTC by dmethvin comment:4

description: It consists primarily in having promise.then implemented like promise.pipe.\ \ We can keep current promise.then under a new alias (attach?) for people who would still want to add callbacks without the cost promise.pipe implies (though we have done, fail and progress for this already).\ \ Change is breaking but only to those who used the value returned by promise.then (either by storing the object or using chaining). I think the tradeof is reasonable enough.It consists primarily in having promise.then implemented like promise.pipe. \ \ We can keep current promise.then under a new alias (attach?) for people who would still want to add callbacks without the cost promise.pipe implies (though we have done, fail and progress for this already). \ \ Change is breaking but only to those who used the value returned by promise.then (either by storing the object or using chaining). I think the tradeof is reasonable enough.

+1, Yeah, we should.

Changed December 14, 2011 03:34AM UTC by dmethvin comment:5

status: newopen

Changed December 14, 2011 04:16PM UTC by timmywil comment:6

description: It consists primarily in having promise.then implemented like promise.pipe. \ \ We can keep current promise.then under a new alias (attach?) for people who would still want to add callbacks without the cost promise.pipe implies (though we have done, fail and progress for this already). \ \ Change is breaking but only to those who used the value returned by promise.then (either by storing the object or using chaining). I think the tradeof is reasonable enough.It consists primarily in having promise.then implemented like promise.pipe.\ \ We can keep current promise.then under a new alias (attach?) for people who would still want to add callbacks without the cost promise.pipe implies (though we have done, fail and progress for this already).\ \ Change is breaking but only to those who used the value returned by promise.then (either by storing the object or using chaining). I think the tradeof is reasonable enough.

-1, The needed functionality is there. I'd rather not make a breaking change right now.

Changed December 14, 2011 04:48PM UTC by rwaldron comment:7

description: It consists primarily in having promise.then implemented like promise.pipe.\ \ We can keep current promise.then under a new alias (attach?) for people who would still want to add callbacks without the cost promise.pipe implies (though we have done, fail and progress for this already).\ \ Change is breaking but only to those who used the value returned by promise.then (either by storing the object or using chaining). I think the tradeof is reasonable enough.It consists primarily in having promise.then implemented like promise.pipe. \ \ We can keep current promise.then under a new alias (attach?) for people who would still want to add callbacks without the cost promise.pipe implies (though we have done, fail and progress for this already). \ \ Change is breaking but only to those who used the value returned by promise.then (either by storing the object or using chaining). I think the tradeof is reasonable enough.

+1, I'm torn, I want great APIs, but I don't want breaking changes

Changed December 15, 2011 07:28AM UTC by mikesherov comment:8

description: It consists primarily in having promise.then implemented like promise.pipe. \ \ We can keep current promise.then under a new alias (attach?) for people who would still want to add callbacks without the cost promise.pipe implies (though we have done, fail and progress for this already). \ \ Change is breaking but only to those who used the value returned by promise.then (either by storing the object or using chaining). I think the tradeof is reasonable enough.It consists primarily in having promise.then implemented like promise.pipe.\ \ We can keep current promise.then under a new alias (attach?) for people who would still want to add callbacks without the cost promise.pipe implies (though we have done, fail and progress for this already).\ \ Change is breaking but only to those who used the value returned by promise.then (either by storing the object or using chaining). I think the tradeof is reasonable enough.

+1

Changed April 01, 2012 11:31PM UTC by jaubourg comment:9

description: It consists primarily in having promise.then implemented like promise.pipe.\ \ We can keep current promise.then under a new alias (attach?) for people who would still want to add callbacks without the cost promise.pipe implies (though we have done, fail and progress for this already).\ \ Change is breaking but only to those who used the value returned by promise.then (either by storing the object or using chaining). I think the tradeof is reasonable enough.It consists primarily in having promise.then implemented like promise.pipe. \ \ We can keep current promise.then under a new alias (attach?) for people who would still want to add callbacks without the cost promise.pipe implies (though we have done, fail and progress for this already). \ \ Change is breaking but only to those who used the value returned by promise.then (either by storing the object or using chaining). I think the tradeof is reasonable enough.

Changed April 25, 2012 11:53PM UTC by domenic@domenicdenicola.com comment:10

+1, please. This is causing a lot of grief over at https://github.com/domenic/chai-as-promised/issues/1

One notable change besides fixing then to work more like pipe is trapping thrown exceptions to make them become rejections.

Changed April 26, 2012 12:54AM UTC by jaubourg comment:11

I don't see jQuery's Deferreds (mis-)handling exceptions the way Promise/A asks for anytime in the future. BTW, I don't see anything about exceptions anymore in the proposal (but it's late and I don't see straight at that hour).

Anyway, here are my 2 cents on the issue.

First, a design based around hindering exceptions is a nightmare when it comes to debugging: there is nothing worse than a program doing nothing in case of an error, especially in an asynchronous environment. I'd rather have the error as vocal as possible to quickly pinpoint the problem: that, to me, is where exceptions shine.

Then, the specific design decision we're talking about is very restrictive from an API point of view unless you wanna get into some unwarantedly convoluted logic.

See, done, fail and progress would have to return a new Promise so that it is in a failed state if and when the given callback throws an exception. Not only does this incur a big performance hit (while, most of the time, you just want to add a callback, not filter/chain anything), but what happens if (like in the case of done, fail and progress) you can provide several callbacks? What is the rejection value if 2 or 3 of them throw an exception? The first exception? All 2 or 3 of them? Should the execution continue for this bunch if and when the first one throws an exception or should it stop right away?

Think it through, there is no good answer.

Not to mention the internal structure needed to keep track of everything (if you're interested in preventing memory build-ups or plain leaks, that is).

Now, let's also see what we're talking about from a coding point of view:

promise.then(function() {
   throw "woops"; 
});

Nothing is thrown. Never mind you say: let's add a callback for that specific case:

promise.then(function() {
   throw "woops"; 
}).then( null, function( whatIsThrown ) {
   throw "another hiccup";
});

And so on, and so on, and good luck finding what causes a problem in your code if and when an exception is thrown deep into the infinite nest of promises.

Now, there are two kinds of exceptions: those you don't expect, those you do expect. I see no value in silencing exceptions you don't expect as I made clear before, but what about programming by exception? I do hope that's not why Promises are supposed to silence exceptions and return a failed Promise.

I guess we're supposed to do things like this:

promise.then(function( data ) {
    if ( !data ) {
       throw "no data";
    }
    return parse( data );
}).then( null, function( error ) {
    console.log( error ); // will HOPEFULLY output "no data"
});

It's comfortable in the sense it looks like synchronous code, but what if parse throws an exception? Yes, you end up having to deal with all possible outcome in the following fail handler. This problem is exactly why Exceptions were invented in the first place: to bypass the usual code flow in case of unexpected failures while providing a mechanism to handle those you do expect and nothing more.

In jQuery 1.8 (and right now if you use pipe instead of then), you can do the following:

promise.then(function( data ) {
    return data
        ? parse( data )
        : $.Deferred().reject( "no data" );
}).then(function( error ) {
    console.log( error ); // will ALWAYS output "no data"
});

You make it explicit you want the following Promise to be rejected and you don't mix applicative and runtime errors.

Changed April 26, 2012 01:26AM UTC by domenic@domenicdenicola.com comment:12

The spec line you missed is

If the callback throws an error, the returned promise will be moved to failed state.


This is really unfortunate to hear. The entire parallel is that sync exceptions <--> async rejections, which is broken and apparently actively rejected by this decision.

I disagree that the point of exceptions is to be vocal and fail as soon as possible; the point is actually to allow you to ignore them until some part of your code is able to handle them. That is why the bubbling behavior is so important, in contrast to e.g. Node.js callback(err, result) solutions.

If there are insurmountable performance concerns about returning a new promise, though, then CommonJS/A compatability is indeed truly broken:

This function should return a new promise

As for the issue of unobserved exceptions never being seen---how is this different from unobserved rejections never being seen?

Finally, your last example doesn't make much sense to me (even assuming you meant to put a null as the first param): parse could return a rejected promise, and then you're back to "HOPEFULLY".


With this in mind, given the numerous Promises/A violations [1] that will cripple interoperability, may I plead with you to remove/deprecate then from the jQuery promise interface? The vast majority of code samples seem to prefer done anyway, so not too much code would be broken.

[1] If you disagree that they are not numerous, I can give you some test suites that fail extensively when I swap Q or when.js for jQuery promises, even after doing a s/.then/.pipe/g.

Changed April 26, 2012 01:39AM UTC by domenic@domenicdenicola.com comment:13

Oh, and another interoperability thing I forgot---this decision makes it impossible to chain jQuery promises in an interoperable way, because you need to take a dependency on jQuery in order to get access to $.Deferred().reject. You cannot write generic promise-based functions like the following:

function ensureHasUsername(promiseForUser) {
  return promiseForUser.then(function (user) {
    if (!user.username) {
      throw new Error("No username!");
    }
  });
}

function doOperationAndDontGiveUp(operation) {
  return operation().then(
    null,
    function (error) {
      if (error instanceof TemporaryNetworkError) {
        console.log("Retrying after error", error);
        return operation();
      }
      throw error;
    }
  });
}

These are just simple examples, but in general it is impossible to use jQuery promises with a "promise aware" library that contains functions that both accept and return promises, in an agnostic way. (Chai as Promised, of course, being my particular case.)

The Promises/A spec is so minimal, but every detail of it has been carefully thought out, with a maximum concern toward building an ecosystem of interoperable promises based solely on the then method. When typeof putativePromise.then === "function" succeeds, but aspects of the spec are not followed, headaches ensue. Again I would advocate for making it explicit that you are opting out of this ecosystem, by removing then.

Changed April 26, 2012 02:56AM UTC by rwaldron comment:14

With this in mind, given the numerous Promises/A violations [1] that will cripple interoperability, may I plead with you to remove/deprecate then from the jQuery promise interface? The vast majority of code samples seem to prefer done anyway, so not too much code would be broken.

This is grossly negligent request. Considering everything else you wrote was very thoughtful, to finish your feedback by asking jQuery to remove part of its API because two "mostly academic" pieces of code aren't interoperable, makes your point go from very helpful to laughably insane.

Really - how often are people writing large scale applications that use Q, when.js _and_ jQuery AND need to pass promises back and forth between the three? I'd say probably none.

Changed April 26, 2012 04:53AM UTC by domenic@domenicdenicola.com comment:15

I'm sorry it came across that way. It certainly didn't seem grossly laughable at the time. Is it more laughable than Promises/A compliance? I honestly thought they would both be backward-incompatible changes and thus on the same level.

And I don't think "mostly academic" is a quote from me, at least?

To answer your use case, the problem isn't people writing applications that use multiple types of promises. We use Q because of jQuery 1.7's shortcomings, and know we need to assimilate any Ajax promises before using so that they have Promises/A behavior. That's fine.

The problem, as I tried to make clear, is library authors, and being part of a larger ecosystem. Currently, no library authors can "trust" jQuery promises. Whether they be Chai as Promised, capsela, wire.js, or taskjs.org, jQuery promises cannot be trusted: they violate the Promises/A contract while faking adherence to it.

As long as such promises are in the ecosystem, libraries must compensate for them. The only way to do this, really, is to mistrust any incoming promise and assimilate it as a $library_authors_favorite_promise_implementation promise. Then the promises coming out of your library functions will be of a different type from those coming into it, leaving lots of people out in the dark.

For example, in Chai as Promised I would wrap all promises passed in with Q.when. But then, users of my library would receive back Q promises, when they passed in jQuery promises, so e.g. they'd have to switch to fin instead of always; the done method would go away; etc. Even worse, well-behaved promises such as those of when.js would be caught in this net of mistrust. Likely only Q users end up using Chai as Promised.

The result is a fragmented ecosystem: Chai as Promised and Capsela treat Q as first-class; wire.js treats when.js as first-class; taskjs.org treats its own custom implementation as first class... And nobody treats jQuery as first class, because it continues to miss the boat on the growing promise ecosystem due to decisions like this one.

Changed April 26, 2012 08:58AM UTC by jaubourg comment:16

_comment0: (rushed answer, since I'm very busy right now and need to get back to stuff that pay the rent, try and understand what is said rather than cherry-pick parts to troll about, thank you) \ \ Replying to [comment:12 domenic@…]: \ > The spec line you missed is \ > \ > > If the callback throws an error, the returned promise will be moved to failed state. \ > \ > ---- \ \ Thanks for pointing this out, it was close to 3am where I was and I couldn't find it. \ \ > \ > This is really unfortunate to hear. The entire parallel is that sync exceptions <--> async rejections, which is broken and apparently actively rejected by this decision. \ > \ > I disagree that the point of exceptions is to be vocal and fail as soon as possible; the point is actually to allow you to ignore them until some part of your code is able to handle them. That is why the bubbling behavior is so important, in contrast to e.g. Node.js `callback(err, result)` solutions. \ > \ \ That's complete non-sense. Exception do not "allow you to ignore them", they plain and simply go over you head UNLESS you catch them. If what you state was through then every javascript statement should be embedded in an if statement: \ \ {{{#!js \ if( typeof TheException !== "thetypeIdontwant" ) { doSomething(); } \ }}} \ \ while thanks to Exception not being caught and put in variables all the time, you code actually is: \ \ {{{#!js \ doSomething(); \ }}} \ \ Which, by the way, is exactly what the Promise/A spec expects you to do in your fail callback. \ \ > If there are insurmountable performance concerns about returning a new promise, though, then CommonJS/A compatability is indeed truly broken: \ > \ > > This function should return a new promise \ > \ \ There is an ENORMOUS performance hit since you need to create a Promise beforehand while waiting for the returned value of whatever handler will fire first, then, if the handler wants to chain with another async operation, another Promise is created which states are piped into the internal one created when "then" was first call. \ \ This is as inefficient as it can get. \ \ Do you realize that, at least half of the time, all you want to do is attach a handler with no interest in filtering/chaining? This is why we first implemented then like we did and waited for 1.6 (see if Deferreds got traction) before introducing pipe. \ \ In 1.8, we will remove the old then and replace it with current pipe. But the very sadening consequence is that we'll have to tell people to use the non-standard done, fail and progress, because the proposal doesn't provide simple, EFFICIENT, mean to just add a callback. \ \ > As for the issue of unobserved exceptions never being seen---how is this different from unobserved rejections never being seen? \ > \ \ The DEVELOPPER performs rejections, the DEVELOPPER AND THE ENVIRONMENT throw exceptions. What is so hard to understand? It's not about the exception YOU throw but about the unexpected exceptions... you know, the ones that will happen whenever your lib is used in the wild. \ \ It's fascinating the way you're just dodging the difference between expected and unexpected exceptions and the clutter it adds in the code if you have to handle them all with if statements. \ \ If you silence runtime non-applicative-relevant exceptions (IE. BUGS), then nobody will use your lib. \ \ > Finally, your last example doesn't make much sense to me (even assuming you meant to put a `null` as the first param): `parse` could return a rejected promise, and then you're back to "HOPEFULLY". \ > \ > --- \ \ Of course, but it's not an issue since it's an EXPECTED outcome of parse while, in the code sample, I made it quite obvious, and ON PURPOSE, that I don't expect parse to throw an exception unless there is a BUG. \ \ Take it this way: you expect the input to be either empty (no data) or well-formed. There is no need for code handling other situations because an ill-formed data is a BUG and you want BUGS to show up. \ \ You keep on assuming ALL exception thrown are relevant to the application logic. 20 years after having been introduced to them, I can tell very few of them are, unless you start throwing them all around as if they were flags, just like Promise/A implicitely advocates. \ \ > \ > With this in mind, given the numerous Promises/A violations [1] that will cripple interoperability, may I plead with you to remove/deprecate `then` from the jQuery promise interface? The vast majority of code samples seem to prefer `done` anyway, so not too much code would be broken. \ > \ > [1] If you disagree that they are not numerous, I can give you some test suites that fail extensively when I swap Q or when.js for jQuery promises, even after doing a s/`.then`/`.pipe`/g. \ \ Most of them are related to jQuery expecting Observables to have a promise method. Don't kid yourself. \ \ Replying to [comment:13 domenic@…]: \ > Oh, and another interoperability thing I forgot---this decision makes it impossible to chain jQuery promises in an interoperable way, because you need to take a dependency on jQuery in order to get access to `$.Deferred().reject`. You cannot write generic promise-based functions like the following: \ > \ > \ > {{{ \ > function ensureHasUsername(promiseForUser) { \ > return promiseForUser.then(function (user) { \ > if (!user.username) { \ > throw new Error("No username!"); \ > } \ > }); \ > } \ > \ > function doOperationAndDontGiveUp(operation) { \ > return operation().then( \ > null, \ > function (error) { \ > if (error instanceof TemporaryNetworkError) { \ > console.log("Retrying after error", error); \ > return operation(); \ > } \ > throw error; \ > } \ > }); \ > } \ > }}} \ > \ > These are just simple examples, but in general it is impossible to use jQuery promises with a "promise aware" library that contains functions that both accept and return promises, in an agnostic way. (Chai as Promised, of course, being my particular case.) \ > \ \ That's complete bullshit. You WILL have a Deferred implementation as a dependency in your code. So, you can write something like this: \ \ {{{ \ function ensureHasUsername( promiseForUser ) { \ return promiseForUser.then(function ( user ) { \ if ( !user.username ) { \ return yourDeferredImplementation() \ [ "yourRejectMethodName" ]( new Error("No username!") ) \ [ "yourPromiseGetter" ](); \ } \ }); \ } \ \ function doOperationAndDontGiveUp( operation ) { \ return operation().then( \ null, \ function ( error ) { \ if ( error instanceof TemporaryNetworkError ) { \ console.log( "Retrying after error", error ); \ return operation(); \ } \ return error; \ } \ }); \ } \ }}} \ \ It should be perfectly interoperable. Of course, it means you have to catch exceptions you EXPECT rather than play it lazy. If you silence exceptions, you doom everyone that will use your lib and have a bug somewhere in their app they need to track down. \ \ When you have a one hour window to find a bug before putting a site in production (you know, when everything works locally but something fails when put in the production environment), I can guarantee you that you don't want any library to hinder exceptions. \ \ Now, when it comes to the example above, it actually doesn't work with jQuery because jQuery expects a promise() method on Observables. I'm open to discussion regarding testing for "then" in addition, so that this kind of code is truly interoperable. \ \ > The Promises/A spec is so minimal, but every detail of it has been carefully thought out, with a maximum concern toward building an ecosystem of interoperable promises based solely on the `then` method. When `typeof putativePromise.then === "function"` succeeds, but aspects of the spec are not followed, headaches ensue. Again I would advocate for making it explicit that you are opting out of this ecosystem, by removing `then`. \ \ I don't think Promise/A has been as carefully thought out as you try to make it sound: \ * no performant means to attach callbacks, \ * when is not part of the standard (which causes REAL interoperability issues), \ * silencing exceptions IS a gigantic mistake, \ * no standard way to mark a non-promise object as Observable through a promise (most libs will have a promise field on such objects, but the approach is weak, especially if the promise has to be dynamically generated, as is the case with [http://api.jquery.com/promise/ fn.promise]), \ * no minimal standard regarding the object that sets the state of a Promise (Deferred), not even naming hints (and now people are attacking us for not using the terms they like btw). \ \ A lot of the grief against jQuery is actually due to shortcomings in the Promise/A spec. I like Kris Zyp very much (we chatted a lot when I was coding $.Deferred) but I think the spec is half-baked. Lots of people are jumping onto the boat before it sailed. I'm afraid it will sink fast because of a lack of reality check (silencing exceptions being the biggest point).1335431254864096

(rushed answer, since I'm very busy right now and need to get back to stuff that pay the rent, try and understand what is said rather than cherry-pick parts to troll about, thank you)

Replying to [comment:12 domenic@…]:

The spec line you missed is > If the callback throws an error, the returned promise will be moved to failed state. ----

Thanks for pointing this out, it was close to 3am where I was and I couldn't find it.

This is really unfortunate to hear. The entire parallel is that sync exceptions <--> async rejections, which is broken and apparently actively rejected by this decision. I disagree that the point of exceptions is to be vocal and fail as soon as possible; the point is actually to allow you to ignore them until some part of your code is able to handle them. That is why the bubbling behavior is so important, in contrast to e.g. Node.js callback(err, result) solutions.

That's complete non-sense. Exception do not "allow you to ignore them", they plain and simply go over you head UNLESS you catch them. If what you state was through then every javascript statement should be embedded in an if statement:

if( typeof TheException !== "thetypeIdontwant" ) { doSomething(); }

Which, by the way, is exactly what the Promise/A spec expects you to do in your fail callback.

While thanks to Exception not being caught and put in variables all the time, you code actually is:

doSomething();
If there are insurmountable performance concerns about returning a new promise, though, then CommonJS/A compatability is indeed truly broken: > This function should return a new promise

There is an ENORMOUS performance hit since you need to create a Promise beforehand while waiting for the returned value of whatever handler will fire first, then, if the handler wants to chain with another async operation, another Promise is created which states are piped into the internal one created when "then" was first call.

This is as inefficient as it can get.

Do you realize that, at least half of the time, all you want to do is attach a handler with no interest in filtering/chaining? This is why we first implemented then like we did and waited for 1.6 (see if Deferreds got traction) before introducing pipe.

In 1.8, we will remove the old then and replace it with current pipe. But the very sadening consequence is that we'll have to tell people to use the non-standard done, fail and progress, because the proposal doesn't provide simple, EFFICIENT, mean to just add a callback.

As for the issue of unobserved exceptions never being seen---how is this different from unobserved rejections never being seen?

The DEVELOPPER performs rejections, the DEVELOPPER AND THE ENVIRONMENT throw exceptions. What is so hard to understand? It's not about the exception YOU throw but about the unexpected exceptions... you know, the ones that will happen whenever your lib is used in the wild.

It's fascinating the way you're just dodging the difference between expected and unexpected exceptions and the clutter it adds in the code if you have to handle them all with if statements.

If you silence runtime non-applicative-relevant exceptions (IE. BUGS), then nobody will use your lib.

Finally, your last example doesn't make much sense to me (even assuming you meant to put a null as the first param): parse could return a rejected promise, and then you're back to "HOPEFULLY". ---

Of course, but it's not an issue since it's an EXPECTED outcome of parse while, in the code sample, I made it quite obvious, and ON PURPOSE, that I don't expect parse to throw an exception unless there is a BUG.

Take it this way: you expect the input to be either empty (no data) or well-formed. There is no need for code handling other situations because an ill-formed data is a BUG and you want BUGS to show up.

You keep on assuming ALL exception thrown are relevant to the application logic. 20 years after having been introduced to them, I can tell very few of them are, unless you start throwing them all around as if they were flags, just like Promise/A implicitely advocates.

>

With this in mind, given the numerous Promises/A violations [1] that will cripple interoperability, may I plead with you to remove/deprecate then from the jQuery promise interface? The vast majority of code samples seem to prefer done anyway, so not too much code would be broken.

>

[1] If you disagree that they are not numerous, I can give you some test suites that fail extensively when I swap Q or when.js for jQuery promises, even after doing a s/.then/.pipe/g.

Most of them are related to jQuery expecting Observables to have a promise method. Don't kid yourself.

Replying to [comment:13 domenic@…]:

Oh, and another interoperability thing I forgot---this decision makes it impossible to chain jQuery promises in an interoperable way, because you need to take a dependency on jQuery in order to get access to $.Deferred().reject. You cannot write generic promise-based functions like the following:
> function ensureHasUsername(promiseForUser) {
>   return promiseForUser.then(function (user) {
>     if (!user.username) {
>       throw new Error("No username!");
>     }
>   });
> }
> 
> function doOperationAndDontGiveUp(operation) {
>   return operation().then(
>     null,
>     function (error) {
>       if (error instanceof TemporaryNetworkError) {
>         console.log("Retrying after error", error);
>         return operation();
>       }
>       throw error;
>     }
>   });
> }
> 
These are just simple examples, but in general it is impossible to use jQuery promises with a "promise aware" library that contains functions that both accept and return promises, in an agnostic way. (Chai as Promised, of course, being my particular case.)

>

That's complete bullshit. You WILL have a Deferred implementation as a dependency in your code. So, you can write something like this:

function ensureHasUsername( promiseForUser ) {
  return promiseForUser.then(function ( user ) {
    if ( !user.username ) {
      return yourDeferredImplementation()
        [ "yourRejectMethodName" ]( new Error("No username!") )
        [ "yourPromiseGetter" ]();
    }
  });
}

function doOperationAndDontGiveUp( operation ) {
  return operation().then(
    null,
    function ( error ) {
      if ( error instanceof TemporaryNetworkError ) {
        console.log( "Retrying after error", error );
        return operation();
      }
      return error;
    }
  });
}

It should be perfectly interoperable. Of course, it means you have to catch exceptions you EXPECT rather than play it lazy. If you silence exceptions, you doom everyone that will use your lib and have a bug somewhere in their app they need to track down.

When you have a one hour window to find a bug before putting a site in production (you know, when everything works locally but something fails when put in the production environment), I can guarantee you that you don't want any library to hinder exceptions.

Now, when it comes to the example above, it actually doesn't work with jQuery because jQuery expects a promise() method on Observables. I'm open to discussion regarding testing for "then" in addition, so that this kind of code is truly interoperable.

The Promises/A spec is so minimal, but every detail of it has been carefully thought out, with a maximum concern toward building an ecosystem of interoperable promises based solely on the then method. When typeof putativePromise.then === "function" succeeds, but aspects of the spec are not followed, headaches ensue. Again I would advocate for making it explicit that you are opting out of this ecosystem, by removing then.

I don't think Promise/A has been as carefully thought out as you try to make it sound:

  • no performant means to attach callbacks,
  • when is not part of the standard (which causes REAL interoperability issues),
  • silencing exceptions IS a gigantic mistake,
  • no standard way to mark a non-promise object as Observable through a promise (most libs will have a promise field on such objects, but the approach is weak, especially if the promise has to be dynamically generated, as is the case with fn.promise),
  • no minimal standard regarding the object that sets the state of a Promise (Deferred), not even naming hints (and now people are attacking us for not using the terms they like btw).

A lot of the grief against jQuery is actually due to shortcomings in the Promise/A spec. I like Kris Zyp very much (we chatted a lot when I was coding $.Deferred) but I think the spec is half-baked. Lots of people are jumping onto the boat before it sailed. I'm afraid it will sink fast because of a lack of reality check (silencing exceptions being the biggest point).

Changed April 26, 2012 05:34PM UTC by domenic@domenicdenicola.com comment:17

_comment0: Replying to [comment:16 jaubourg]: \ \ OK, wow, it looks like this discussion has taken a turn for the acrimonious. I'm sorry for my part in that, and appreciate the time you've put in replying to me so far. I ensure you my intention was never to troll, to distract you from paying the rent, or to be grossly negligent. \ \ It's clear you've made up your mind on these points, and I do understand your position and reasons for it—I simply disagree with them, and thus argued against them. On reflection, "yes, but" breeds a better discussion than "but" alone, so apologies for taking that latter tack. \ \ Since I do understand your reasons, and recognize I'm not going to change your mind, I'd like to just take some time to help you understand my position. If we can agree to disagree, I'll leave it at that. \ \ I would appreciate a reasoned response that takes the time to read and think about my points, so if that means waiting days for a reply until you have more time to respond without feeling rushed, please do so. I recognize that, while promises are a passion of mine (Q is the most-watched library I collaborate with on GitHub), they are only a tiny portion of jQuery, so I do not expect you to match my response time. \ \ ---- \ \ > That's complete non-sense. Exception do not "allow you to ignore them", they plain and simply go over you head UNLESS you catch them. \ \ I think we are talking past each other. It's exactly that "going over your head" behavior that I want in a promise setting. The problem comes, I think, that you do not accept the Promises/A equivalence between rejection handlers and `catch` blocks. As for the issue of exception type-checking, I can assure you that this is not how Promise/A-using code works: like `catch` blocks, rejection handlers are inserted only at seams between systems, where usually any error at all is cause for alarm and handling. \ \ > There is an ENORMOUS performance hit \ \ I admit the jQuery team is much more knowledgable about the vagaries of JavaScript performance. You presumably have a much broader view of what's important and what's not, and in what setting (mobile etc.). That said, we are using Q promises in a 170K LOC HTML5 desktop application, as well as in a (much smaller) Node.js server, and they have never been even close to a performance bottleneck. This seems especially true in the case of jQuery, where promises are used to react to Ajax, animations, etc. which will obviously dwarf the overhead of creating a JavaScript object and piping states around. But I admit, you guys have a much better grasp on the performance implications, e.g. maybe creating objects is more expensive on mobile WebKit. \ \ > In 1.8, we will remove the old then and replace it with current pipe. But the very sadening consequence is that we'll have to tell people to use the non-standard done, fail and progress, because the proposal doesn't provide simple, EFFICIENT, mean to just add a callback. \ \ It is great to hear that you'll continue the trend of encouraging non-`then` usage; this will help clarify the division. Dare I ask you, jaubourg, about how you feel w.r.t. depracating `then`? (I'd like to not get laughed at again though, so if you think it's absurd just ignore that query.) \ \ As for efficient means to just add a callback, I thought that was what `$.Callbacks` was for? I guess there might be some historical issues here that are too late to address. \ \ > The DEVELOPPER performs rejections, the DEVELOPPER AND THE ENVIRONMENT throw exceptions. What is so hard to understand? It's not about the exception YOU throw but about the unexpected exceptions... you know, the ones that will happen whenever your lib is used in the wild. \ \ > It's fascinating the way you're just dodging the difference between expected and unexpected exceptions and the clutter it adds in the code if you have to handle them all with if statements. \ \ > If you silence runtime non-applicative-relevant exceptions (IE. BUGS), then nobody will use your lib. \ \ Again, the intent of Promises/A is that there is no difference: rejections should be no more or less powerful than exceptions, but instead should simply replace exceptions in the async context. There are unexpected rejections, just as there are unexpected exceptions, and nobody clutters their asynchronous code with `if` statements in their rejection handlers any more than people clutter their synchronous code with `if` statements in their `catch` blocks. \ \ I think the line between developer and environment is much more blurry than you are trying to make it. When I use a third-party asynchronous library that returns rejected promises, is that the developer or the environment? What about a third-party synchronous library that throws exceptions? If that third-party library returns a rejected promise, but I forget to observe rejection, this will cause the same problems that "forgetting" to wrap third-party library code in `try`/`catch` does. Yes, there is a difference, I admit: if your fulfillment handler does a `undefined.foo = "bar"`, that's clearly an environment-thrown exception. But you'd need another `try`/`catch` to handle that in a synchronous workflow (wherein it would be your `catch` block being boneheaded), so why the resistance to adding another rejection handler in an asynchronous one? \ \ As for the silencing issue, I think you are referring to the problem wherein unobserved rejections that bubble all the way to the top need some way of making their presence known, just like unobserved exceptions do. This is solved in various ways by various promise libraries: in Q we have provided the `end` capping method [1] and, in master, a live console-list of unobserved rejections. In WinJS promises (Windows 8's built-in promise framework for writing desktop apps) you can use an explicitly non-chaining `done` method, or a `WinJS.promise.onerror` handler. (Microsoft actually has a great article about this problem [3]—really highly recommended reading, although I know you are busy). From my understanding you find the tradeoff to be too great of a price to pay for the potential of lost exceptions, and that's fine. But it's a tradeoff that those using Promises/A promises have chosen to buy into. \ \ [1]: http://documentup.com/kriskowal/q/#tutorial/the-end \ [2]: http://msdn.microsoft.com/en-us/library/windows/apps/hh701079.aspx \ [3]: http://msdn.microsoft.com/en-us/library/windows/apps/hh700337.aspx \ \ > That's complete bullshit. You WILL have a Deferred implementation as a dependency in your code. \ \ I really, strongly, disagree in the case of library authors. I think if you re-read what I was trying to say, this will become more clear. Deferreds are only necessary for adapting callback-style code. Once you have promises in your system, everything can be done with chaining in a library-agnostic way, which is why Promises/A is so powerful. I gave several examples of potential utility functions that promise-based libraries could provide. And of course I'll haul out Chai as Promised once again: it allows you to make assertions about the state of promises that then become promises, e.g. \ \ {{{ \ expect(promise).to.be.rejected.with(TypeError).then( \ function () { \ // promise was rejected with TypeError \ }, \ function (err) { \ // err contains an AssertionError saying "expected TypeError but was WhateverError" \ } \ ); \ assert.eventually.deepEqual(promise, { foo: { bar: "baz" } }).then( \ function () { \ // promise was fulfilled with something that was deepEqual to { foo: { bar: "baz" } } \ }, \ function (err) { \ // err contains AssertionError. \ } \ ); \ }}} \ \ This is accomplished with zero dependency on a deferred library, and simply uses the `.then` function of whatever it is given. It has an extensive test suite that passes with Q promises, When.js promises, and WinJS promises, but fails with jQuery promises even after s/`.then`/`.pipe`/g (and, from a glance, none of the failures are related to expected a `.promise` method). \ \ > It should be perfectly interoperable. \ \ Your first rewritten example takes on a deferred dependency, so the returned promise will not be of the type passed in: now nobody wants to use my library, because they have to learn how to use Q promises when they were using WinJS promises all along. Your second example will not work with any Promises/A compliant promises, since it returns the error instead of throwing it, thus causing the promise to be fulfilled (*now* we're swallowing exceptions :P). This is part of why I say that Promises/A has really thought this out: it's not easy to get such things right. \ \ > I don't think Promise/A has been as carefully thought out as you try to make it sound: \ \ Promises/A is the minimal spec. Some of what you are referring to is addressed by Promises/B [4] or UncommonJS promises [5]. Of course, neither of these specs have gotten much traction, because just getting people to agree to the basics of Promises/A is (1) hard enough; (2) powerful enough to enable interoperability. \ \ [4]: http://wiki.commonjs.org/wiki/Promises/B \ [5]: http://kriskowal.github.com/uncommonjs/promises/specification \ \ Yes, Promises/A not perfect—especially if your priorities are not a direct synchronous-to-asynchronous correspondence—but I hope you can see why it has its proponents. If jQuery wants to go off in a different direction, that's fine, and I understand, but I just want to make sure I've helped you see the tradeoffs being made. It sounds like you are aware of most of them, and that jQuery intends to, in effect, cut itself off from the Promises/A ecosystem and encourage its own ecosystem, in which performance and never accidentally silencing exceptions are a higher priority than composability and interoperability. \ \ ---- \ \ Thank you for your time, and sorry for making you read such a long treatise. As I said, promises are a passion of mine; I evangelize them heavily in the New York JavaScript community, and hate having to tell people to avoid jQuery promises when I do so. I hope I haven't exhausted your patience, and do appreciate that you're willing to engage with the Promises/A community like this. \ 1335461717847998

Replying to [comment:16 jaubourg]:

OK, wow, it looks like this discussion has taken a turn for the acrimonious. I'm sorry for my part in that, and appreciate the time you've put in replying to me so far. I ensure you my intention was never to troll, to distract you from paying the rent, or to be grossly negligent.

It's clear you've made up your mind on these points, and I do understand your position and reasons for it—I simply disagree with them, and thus argued against them. On reflection, "yes, but" breeds a better discussion than "but" alone, so apologies for taking that latter tack.

Since I do understand your reasons, and recognize I'm not going to change your mind, I'd like to just take some time to help you understand my position. If we can agree to disagree, I'll leave it at that.

I would appreciate a reasoned response that takes the time to read and think about my points, so if that means waiting days for a reply until you have more time to respond without feeling rushed, please do so. I recognize that, while promises are a passion of mine (Q is the most-watched open source library I am a contributor on), they are only a tiny portion of jQuery, so I do not expect you to match my response time.


That's complete non-sense. Exception do not "allow you to ignore them", they plain and simply go over you head UNLESS you catch them.

I think we are talking past each other. It's exactly that "going over your head" behavior that I want in a promise setting. The problem comes, I think, that you do not accept the Promises/A equivalence between rejection handlers and catch blocks. As for the issue of exception type-checking, I can assure you that this is not how Promise/A-using code works: like catch blocks, rejection handlers are inserted only at seams between systems, where usually any error at all is cause for alarm and handling.

There is an ENORMOUS performance hit

I admit the jQuery team is much more knowledgable about the vagaries of JavaScript performance. You presumably have a much broader view of what's important and what's not, and in what setting (mobile etc.). That said, we are using Q promises in a 170K LOC HTML5 desktop application, as well as in a (much smaller) Node.js server, and they have never been even close to a performance bottleneck. This seems especially true in the case of jQuery, where promises are used to react to Ajax, animations, etc. which will obviously dwarf the overhead of creating a JavaScript object and piping states around. But I admit, you guys have a much better grasp on the performance implications, e.g. maybe creating objects is more expensive on mobile WebKit.

In 1.8, we will remove the old then and replace it with current pipe. But the very sadening consequence is that we'll have to tell people to use the non-standard done, fail and progress, because the proposal doesn't provide simple, EFFICIENT, mean to just add a callback.

It is great to hear that you'll continue the trend of encouraging non-then usage; this will help clarify the division. Dare I ask you, jaubourg, about how you feel w.r.t. depracating then? (I'd like to not get laughed at again though, so if you think it's absurd just ignore that query.)

As for efficient means to just add a callback, I thought that was what $.Callbacks was for? I guess there might be some historical issues here that are too late to address.

The DEVELOPPER performs rejections, the DEVELOPPER AND THE ENVIRONMENT throw exceptions. What is so hard to understand? It's not about the exception YOU throw but about the unexpected exceptions... you know, the ones that will happen whenever your lib is used in the wild.
It's fascinating the way you're just dodging the difference between expected and unexpected exceptions and the clutter it adds in the code if you have to handle them all with if statements.
If you silence runtime non-applicative-relevant exceptions (IE. BUGS), then nobody will use your lib.

Again, the intent of Promises/A is that there is no difference: rejections should be no more or less powerful than exceptions, but instead should simply replace exceptions in the async context. There are unexpected rejections, just as there are unexpected exceptions, and nobody clutters their asynchronous code with if statements in their rejection handlers any more than people clutter their synchronous code with if statements in their catch blocks.

I think the line between developer and environment is much more blurry than you are trying to make it. When I use a third-party asynchronous library that returns rejected promises, is that the developer or the environment? What about a third-party synchronous library that throws exceptions? If that third-party library returns a rejected promise, but I forget to observe rejection, this will cause the same problems that "forgetting" to wrap third-party library code in try/catch does. Yes, there is a difference, I admit: if your fulfillment handler does a undefined.foo = "bar", that's clearly an environment-thrown exception. But you'd need another try/catch to handle that in a synchronous workflow (wherein it would be your catch block being boneheaded), so why the resistance to adding another rejection handler in an asynchronous one?

As for the silencing issue, I think you are referring to the problem wherein unobserved rejections that bubble all the way to the top need some way of making their presence known, just like unobserved exceptions do. This is solved in various ways by various promise libraries: in Q we have provided the end capping method [1] and, in master, a live console-list of unobserved rejections. In WinJS promises (Windows 8's built-in promise framework for writing desktop apps) you can use an explicitly non-chaining done method, or a WinJS.promise.onerror handler. (Microsoft actually has a great article about this problem [3]—really highly recommended reading, although I know you are busy). From my understanding you find the tradeoff to be too great of a price to pay for the potential of lost exceptions, and that's fine. But it's a tradeoff that those using Promises/A promises have chosen to buy into.

[1]: http://documentup.com/kriskowal/q/#tutorial/the-end

[2]: http://msdn.microsoft.com/en-us/library/windows/apps/hh701079.aspx

[3]: http://msdn.microsoft.com/en-us/library/windows/apps/hh700337.aspx

That's complete bullshit. You WILL have a Deferred implementation as a dependency in your code.

I really, strongly, disagree in the case of library authors. I think if you re-read what I was trying to say, this will become more clear. Deferreds are only necessary for adapting callback-style code. Once you have promises in your system, everything can be done with chaining in a library-agnostic way, which is why Promises/A is so powerful. I gave several examples of potential utility functions that promise-based libraries could provide. And of course I'll haul out Chai as Promised once again: it allows you to make assertions about the state of promises that then become promises, e.g.

expect(promise).to.be.rejected.with(TypeError).then(
    function () {
        // promise was rejected with TypeError
    },
    function (err) {
        // err contains an AssertionError saying "expected TypeError but was WhateverError"
    }
);
assert.eventually.deepEqual(promise, { foo: { bar: "baz" } }).then(
    function () {
        // promise was fulfilled with something that was deepEqual to { foo: { bar: "baz" } }
    },
    function (err) {
        // err contains AssertionError.
    }
);

This is accomplished with zero dependency on a deferred library, and simply uses the .then function of whatever it is given. It has an extensive test suite that passes with Q promises, When.js promises, and WinJS promises, but fails with jQuery promises even after s/.then/.pipe/g (and, from a glance, none of the failures are related to expected a .promise method).

It should be perfectly interoperable.

Your first rewritten example takes on a deferred dependency, so the returned promise will not be of the type passed in: now nobody wants to use my library, because they have to learn how to use Q promises when they were using WinJS promises all along. Your second example will not work with any Promises/A compliant promises, since it returns the error instead of throwing it, thus causing the promise to be fulfilled (*now* we're swallowing exceptions :P). This is part of why I say that Promises/A has really thought this out: it's not easy to get such things right.

I don't think Promise/A has been as carefully thought out as you try to make it sound:

Promises/A is the minimal spec. Some of what you are referring to is addressed by Promises/B [4] or UncommonJS promises [5]. Of course, neither of these specs have gotten much traction, because just getting people to agree to the basics of Promises/A is (1) hard enough; (2) powerful enough to enable interoperability.

[4]: http://wiki.commonjs.org/wiki/Promises/B

[5]: http://kriskowal.github.com/uncommonjs/promises/specification

Yes, Promises/A not perfect—especially if your priorities are not a direct synchronous-to-asynchronous correspondence—but I hope you can see why it has its proponents. If jQuery wants to go off in a different direction, that's fine, and I understand, but I just want to make sure I've helped you see the tradeoffs being made. It sounds like you are aware of most of them, and that jQuery intends to, in effect, cut itself off from the Promises/A ecosystem and encourage its own ecosystem, in which performance and never accidentally silencing exceptions are a higher priority than composability and interoperability.


Thank you for your time, and sorry for making you read such a long treatise. As I said, promises are a passion of mine; I evangelize them heavily in the New York JavaScript community, and hate having to tell people to avoid jQuery promises when I do so. I hope I haven't exhausted your patience, and do appreciate that you're willing to engage with the Promises/A community like this.

Changed June 18, 2012 03:30PM UTC by dmethvin comment:18

resolution: → fixed
status: openclosed
summary: Make Deferred implementation truly Promise/A compliantMake Deferred.then == Deferred.pipe like Promise/A

I've retitled the ticket to describe its actual goal, which was accomplished here: https://github.com/jquery/jquery/commit/a41f2406748e3113751ab1e5b5d990d9144123fc

We cannot become compliant with other implementations of the interpretations of Promise/A without breaking a lot of existing jQuery user code. We do not want to do that. They will get mad and yell at us. Sorry.

Changed June 18, 2012 03:58PM UTC by jaubourg comment:19

Given the implementation is not Promise/A compatible, shouldn't we leave jQuery's Deferred.then as it is and go back to having pipe for chaining/filtering? Wouldn't it make sense not to introduce a breaking change for pretty much no benefit? This ticket was introduced and voted for on the assumption it would make our Promises "compliant enough" but it's not that simple... I'm on the road and cannot attend the meeting nor make the necessary changes but it would make sense and gibson is perfectly abke to do this if needs be.