domenic / promises-unwrapping

The ES6 promises spec, as per September 2013 TC39 meeting
1.23k stars 94 forks source link

Promise.prototype.done #19

Closed medikoo closed 10 years ago

medikoo commented 10 years ago

With current design, the only way to process resolved value and be sure that unhandled exceptions are exposed, is via following

promise.then(function (value) {
  setTimeout(function () {
    // Process value
   }, 0);
}, function (err) {
  setTimeout(function () {
    // Handle error
   }, 0);
});

Which is poor solution for many reasons:

In many promise implementations, there's already promise.done, which doesn't create any new promise, and invokes callbacks naturally (outside of try/catch clause).

As done cannot be implemented on top of available API, I think it's very important to include it in spec.

As side note, why do Promise.prototype.catch is specified? It can be very easily configured on top of the rest of the API, and it's usability is low.

annevk commented 10 years ago

The outcome of the previous TC39 discussion was to get rid of done() for now. It might resurface one way or another, but adding it back before revisiting that discussion seems bad, and we can easily revisit that after we ship the basic design.

medikoo commented 10 years ago

@annevk Thanks for clarification. I just wanted to point that without that, spec (even in most basic form) shouldn't be perceived as complete.

domenic commented 10 years ago

done's job will be done by integrating unhandled rejection tracking functionality into dev tools. Most TC39ers, from what I understand, as well as myself, perceive that as enough for the spec to be complete.

Furthermore, there is no need for implementations to create promise objects only to have them immediately GCed. Unlike user-space promise implementations, that is easily detectable; such then invocations whose return values are ignored can simply not create a new promise object.

medikoo commented 10 years ago

done's job will be done by integrating unhandled rejection tracking functionality into dev tools. Most TC39ers, from what I understand, as well as myself, perceive that as enough for the spec to be complete.

Monitoring solution significantly raises complexity of implementation, and brings many other issues. I haven't seen it yet either implemented or specified in a way that would really solve above issue.

It's really hard to understand for me, why you advocate after something that is difficult to implement, not logical and not natural for JavaScript, when real solution can be so simple, and as far as I remember from discussion on es-discuss, many were concerned about that.

Furthermore, there is no need for implementations to create promise objects only to have them immediately GCed. Unlike user-space promise implementations, that is easily detectable

Can you explain exactly, how? Static code analysis?

annevk commented 10 years ago

Implementations can report unhandled exceptions during GC to the error console.

medikoo commented 10 years ago

@annevk You mean that when promise would be GC'ed, implementation would check whether reject value was retrieved and if not, it would report to reject value to error console (?)

What if implementation would completely implement this spec, but won't provide this functionality? I hope you understand that such implementation would turn out as hardly usable.

...and again it's not error handling that JS programmers are used to, it can be simple and should be in my opinion.

annevk commented 10 years ago

If implementations do not provide adequate debugging, they'll be used less. This is the same for any number of features on the platform. And simple is not necessarily good here, as people will use .then() and forget about things.

medikoo commented 10 years ago

@annevk only with done programmer can have full control over error handling, and promise libraries should provide that.

Argument that having both then and done is too much for programmer's head is quite controversial to me. Both methods serve two different use cases, and both are equally important.
Also looking at other API examples you can see it's not true. e.g. programmers somehow manage well both map and forEach on arrays. I haven't seen complains that we should remove forEach and just use map, as it's confusing and implementation might automatically not create returned arrays if it detects it's not taken.

annevk commented 10 years ago

Per http://lists.w3.org/Archives/Public/public-script-coord/2013JulSep/0571.html we should reconsider this.

domenic commented 10 years ago

Might be time for @wycats to step in, as he's one of the most prominent anti-done people.

wycats commented 10 years ago

I don't feel like arguing with BE. I strongly believe that this is a scenario solving mistake that we are hastily making as promises have gained popularity.

I have come to believe that the fact that done is needed at all is a fatal flaw in the current specification. Swallowing honest-to-god exceptions (TypeError, ReferenceError) as though they were promise rejections was just a mistake.

At minimum, it should be possible to configure a promise library to bubble out exceptions, for debugging.

I would like it if we didn't repeat this mistake with DOMPromises, and avoid compounding the mistake by complicating the API.

mhofman commented 10 years ago

I've been reading and thinking a lot about this error / rejection handling issue in recent days and I've become convinced that the answer is in keeping Promise's semantics simple so that it mirrors synchronous calls.

In that sense, then is used to execute another operation after the previous one completes and catch is used to handle an operation failing. For the same reason, I would also like to see finally as I expressed before in #18 but I guess I could live without it for now.

Discriminating on the failure's type is a dangerous business since I doubt we can be sure a TypeError / ReferenceError is always due to a promise's consumer. For example, what if such an error had actually bubbled from inside a library the consumer is using? Should the consumer's asynchronous flow be interrupted even if he had a catch in place for handling such errors?

I think the burden of making sure asynchronous operations are as easy to debug as synchronous ones lies with the browser / JavaScript engine. Logging unhandled rejections is the most obvious aspect of this but there are plenty of other things that can be done to improve the developer's experience. For example engines could provide the asynchronous call stack when using promises. This is a feature available in the newest Visual Studio 2013 and it supports WinJS promises (http://blogs.msdn.com/b/visualstudioalm/archive/2013/07/01/debugging-asynchronous-code-in-visual-studio-2013-call-stack-enhancements.aspx)

Asking the developer to finish their asynchronous flow with a done creates the potential for them to forget to do so just as much as they now forget to finish it with a catch.

domenic commented 10 years ago

I agree with everything @mhofman says, with the caveat that I am not sure what the exact solution would be for consoles (i.e. non-interactive debuggers). I would like a solution that does not affect the authoring-time experience, though.

wycats commented 10 years ago

The try/catch that wraps the success and failure handlers is catching errors that by definition were triggered by consumer code.

annevk commented 10 years ago

@erights, @BrendanEich, way in maybe? Still feels like something we can address later.

mhofman commented 10 years ago

Yes, those errors are triggered by JavaScript code, but they don't automatically originate in the code written by the developer (consumer of the promise). My point was that a developer might want to handle such errors himself.

Regarding console type environments, couldn't the unhandled rejection be considered as a failure? I'm not sure of the consequences but for example right now an error in a setTimeout in node.js will cause the program to terminate.

wycats commented 10 years ago

@mhofman yep. Bottom line, many these issues are best handled by debugging environments, but as promises have quickly become more popular, and debuggers haven't caught up, people are trying to slam in the .done() solution to PATCH IT NOW.

I'm working on the Ember Promises debugger (for the Ember Inspector) and have talked with Dave Camp a bunch about plans for the Mozilla debugger, and making API decisions right now (before even the first iteration of debugger tools for promises have shipped) strikes me as very short-sighted.

That's why my proposals are almost all modes that you can turn on in debugging as a stopgap, and not permanent changes to the core spec.

Nathan-Wall commented 10 years ago

As someone who spent a long time thinking throw -> reject was a mistake in promise design, I have done a 180 fairly recently and wholeheartedly support the no-done-patch sentiments in this thread. There was a time when browsers didn't have good debuggers for regular errors; that's just where we happen to be right now with promises. Adding done to the language just so that you can see certain errors in modern debuggers would be akin to teaching people to alert all errors in 2003 instead of using throw because they were practically invisible (or impossible to get the right message/line numbers from) back then. I mean, alert was what you had to use when you were debugging, but it would have been bad design to use it in general purpose code because it'd be impossible to recover from it. Since an error thrown in done is impossible to recover from, it would be bad design to use, and shouldn't be part of the language.

Like Anne said, implementations that don't provide good debuggers for promises won't be used to debug problems in async control flow (or they'll be patched with tools like Firebug).

ForbesLindesay commented 10 years ago

I don't want .done for debugging purposes (that's a useful side effect). I want it for two reasons:

  1. It lets me be explicit about what I want to happen (just like I still use .forEach whenever I don't want to make use of the result I'd get via .map.
  2. It is an extremely useful tool for explaining .then. I just gave a talk (2 days ago) on promises and described .done first, and then described .then as a helper method for .done when you need a new promise as the result. Everyone in that room instantly understood how .then works and what it does. Explaining .then first doesn't work. Explaining .map without first explaining .forEach doesn't work.

We need .done so we can explain what .then does.

ghost commented 10 years ago

@Nathan-Wall Your analogy to primordial JS debugging breaks down when you realize that the root problem is that under the current paradigm, there is no possible way to distinguish an "unhandled" rejection from a program error. "Monitoring" tools attempt to sidestep this difficulty by placing the burden of distinction upon the programmer at the time of debugging. But programming languages should be designed so that the programmer can state his or her intentions in the source code itself.

Some random thoughts:

annevk commented 10 years ago

A lot of those seem false. We ship a lot of APIs that for debugging require non built-in features. E.g. lots of network APIs don't expose sufficient information for easy debugging (due to not wanting to reveal that information).

medikoo commented 10 years ago

Asking the developer to finish their asynchronous flow with a done creates the potential for them to forget to do so just as much as they now forget to finish it with a catch.

@mhofman It's not like that. The problem is that you treat done as a supplement of then which is logically wrong . then is a mapping function that developer should choose over done and not otherwise. done should be treated as first choice function. It's the fundamental, straightforward way to access resolved value. It is then that someone may forgot about not done.

So, current state of the API provides us with brilliantly designed mapping function, but we miss one that allows us to process/access resolved values natural way, and agenda is to force user to use then for that, and fix its flaws with not natural, implementation dependent, mechanism that can't be implemented with plain JavaScript. Doesn't it give you notion there's some logical flaw here?

As a developer I expect that the API provides me with fine error handling control, not specifying done you're taking that away and I think there's no other JavaScript API that does that.

domenic commented 10 years ago

@medikoo, I get that you have different ideas for how you would design and use a promise API. You fail to recognize that not everyone wants to use your library's paradigm.

medikoo commented 10 years ago

@domenic this has nothing to do with my library, it's implementation of done just follows others. It'll be also great if you'd have more open and constructive approach, your negative attitude doesn't help.

ghost commented 10 years ago

@annevk True, to a certain extent. The difference, to me, is that when debugging this:

function someFunction() { ... }
promise.then(val => someFunctoin(val));

I need immediate feedback. I don't want to write boilerplate (i.e. done, or setTimeout) and I certainly don't want to tab over to a promise monitoring panel to figure out why nothing is happening. This is a program error. The programming environment should be able to figure out when I've made this kind of error and notify me accordingly.

I think punting on done is understandable, but it may be that by punting on done we assure it's victory. As @erights has said, a warty done still looks better that the proposed alternatives. And that has been the state of things for years. This indicates to me that something is missing from our current design with respect to error handling.

Is that missing something GC? I don't know, but I think it would be worth our time to explore the alternatives and implications.

annevk commented 10 years ago

The error there is the typo in someFunction, right?

I don't understand how by punting on done we assure its victory. What strategy other than the implementation layer and something akin to done are there?

mhofman commented 10 years ago

@zenparsing Unfortunately the behavior you're asking for is not possible with JavaScript. Consider a similar example without the use of promises:

function someFunction() { ... }
setTimeout(() => someFunctoin(123), 0);

The ReferenceError will only be thrown when the function passed to setTimeout is executed, not when setTimeout is called. Today, to figure out there is a typo here, you need the function to be executed and then look at your console. Pormise monitoring tools would offer the same kind of error reporting.

The only way to fail at parsing in this case would be to disallow functions dynamically added to the global from being executed.

ForbesLindesay commented 10 years ago
  1. forEach is the basic way of getting values out of an array, not map
  2. done should be the basic way of getting a value out of a promise, not then

I don't see how you can dispute the second statement without disputing the first. Does anyone dispute the first statement? Does anyone see a reason to dispute the second without disputing the first?

ForbesLindesay commented 10 years ago

That doesn't mean that map won't be more popular than forEach or that then won't be more popular than done, but that wouldn't be an argument not to have done, unless you also think we shouldn't have had forEach.

domenic commented 10 years ago

I don't think the promise <-> array analogy holds up very well under scrutiny.

medikoo commented 10 years ago

@domenic can you be more constructive, do you have any valid arguments?

annevk commented 10 years ago

It's not about whether or not to return a new promise. It's about reporting errors. The forEach/map analogy has nothing on that.

ghost commented 10 years ago

@mhofman I meant the same ReferenceError that setTimeout would report on the console. There's no reason why I should have to add boilerplate (done) or tab between two different views (the console and the promise monitor) in order to see that error. It is a programming error and should be presented to the programmer in the same way as other programming errors.

medikoo commented 10 years ago

@annevk It's not only about that, thing that promise.then creates another promise that we totally don't need, affects for performance and is not clean. Also the reason of proposed error reporting is mapping feature of then, this comes out of it.

You want us to deal with all those side effects of map on each value access. I really wouldn't like to be forced to usesetTimeout with native API's to be able to handle errors normal way. It's bad idea to leave API in such state.

domenic commented 10 years ago

promise.then will not create a new promise in any reasonable optimizing JIT, unless its return value is actually consumed.

medikoo commented 10 years ago

@domenic it's weird thing to provide imperfect solution and fix its issues that way. It's another mess to handle for compiler. Also mind that such thing can't be implemented in plain JavaScript (no polyfill possible).

ghost commented 10 years ago

@annevk I'll try to explain...

First, let's forget about the browser and just consider the server. The appropriate thing for a console (Node) application to do when it encounters an unhandled error is to crash the program, reporting some error information. You don't fire up a monitor.

So the only way that the current promises spec will work in that environment is to include boilerplate (done), or to rely on the GC to deduce that an unhandled rejection will never be handled, and therefore constitutes a program error.

To my knowledge, we've not fully validated the GC approach. If it turns out that we can't rely on the GC, then the only other option for that environment is done.

The force which is pushing us toward the GC or done is the fact that, as currently specified, one may attach a listener to a rejected promise in an arbitrarily distant future. This is the property which is wreaking havoc with our ability to report simple programming errors. Is this property required?

annevk commented 10 years ago

Let's make that more concrete.

The suggestion would be that in PropagateToDerived(p) if p.[[Derived]] is empty and promise.[[Reason]] is set, you'd propagate it out of the promise? (E.g. window.onerror, console, ...)

wycats commented 10 years ago

@annevk I implement a similar strategy to this in RSVP (via RSVP.configure('onerror')). Unfortunately, it makes for a lot of noise in cases where people are intentionally trying to attach error handlers later.

I consider it worth the trouble for debugging, which is why I offer the option. I've had good luck printing error messages to the console in my implementations of the RSVP onerror hook only if the original error has a stack (i.e. it's a real error), but clearly these are only heuristics that can be overly noisy or overly silent depending on the use-case.

fwiw: The fact that people don't typically use debugger tools in node yet doesn't mean that we should assume node will never have a popular, good debugger environment.

annevk commented 10 years ago

Okay, so @wycats' comment indicates that even if we did want that, it'd have to be opt-in, and thus could be added at a later stage. So we have three strategies: 1) done() 2) GC/engine magic 3) a flip to twiddle PropagateToDerived handling. All three can be deployed after-the-fact. Unless there's anything else, I recommend we close this.

medikoo commented 10 years ago

The suggestion would be that in PropagateToDerived(p) if p.[[Derived]] is empty and promise.[[Reason]] is set, you'd propagate it out of the promise? (E.g. window.onerror, console, ...)

As @wycats noted it is imperfect solution, and I think it shouldn't be taken into account

So we have three strategies: 1) done() 2) GC/engine magic 3) a flip to twiddle PropagateToDerived handling. All three can be deployed after-the-fact

If promise implementation would implement in full given API but would not provide any of above, would you consider it as complete and fine to use?

Other important thing we need to realize, if some implementations will do 1), and others will do 2) we will deal with promises implementations that would conform to same API but would need to be used in different way. That means, code written for A would not work as expected in B, and other way, and we're not talking about optional custom features, it's about errors handling which is mandatory thing to solve. We're introducing disruption.

annevk commented 10 years ago

That's bullshit. Implementations are not allowed to deviate from the standard. The implementers know at this point in time their only option is 2. We can then evaluate later on whether we actually want 1, 2, or 3 or a combination. It's not about complete, software never is, it's about what we can ship right now and start playing without creating problems going forward.

medikoo commented 10 years ago

That's bullshit. Implementations are not allowed to deviate from the standard.

It's exactly my point. We need to specify that, so we have one solution and not few different and incompatible (lack of specification leads to deviations).

The implementers know at this point in time their only option is 2.

Is it specified somewhere, or just decided behind the curtains? Implementers of which engines agreed on that?

It's not about complete, software never is

We're talking about mandatory functionality that will make this API usable. It's Promise.prototype.catch that can be ignored and added at any time later, but not Promise.prototype.done.

ForbesLindesay commented 10 years ago

I keep failing to get people to see my point. The analogy between array and promise holds up brilliantly in this case. I'm not arguing for something to help error handling. I'm not arguing for something to help performance by not creating an additional promise (I've implemented done in terms of then for my promise implementation). I'm arguing for a method that lets me say what I mean. I want to express my intent when writing the function. That is to say, I want to express the difference between "Get the result of this promise" and "Transform this promise with an additional operation".

ForbesLindesay commented 10 years ago

Even if we could solve the halting problem and always detect promises that would never be handled, I'd still want done so I could express my intent.

juandopazo commented 10 years ago

@ForbesLindesay FWIW it's not just about an analogy with arrays. Other languages have both features. For example Scala has onComplete and map.

erights commented 10 years ago

Even if we could solve the halting problem and always detect promises that would never be handled, I'd still want done so I could express my intent.

@ForbesLindesay FWIW, I expect to want it too. But it would be a compatible addition, and so can come in standards that build on this one.

It's [P]romise.prototype.catch that can be ignored and added at any time later, but not [P]romise.prototype.done.

@medikoo I agree with the first part of this statement. I also agree that the argument for including .catch is weaker than that for .done, so including .catch and not .done seems mistaken. If you want to start an issue to remove .catch from this repository, I would be in favor. .catch is also something that would be a compatible addition later and so need not happen in this round.

medikoo commented 10 years ago

@erights To me catch is redundant to then not done, it's actually just sugar that can easily be implemented on top of then (edit: sorry misunderstood your point at first)

I don't mind catch in the API, it's just surprising to see such optional function being added and done being ignored.

ForbesLindesay commented 10 years ago

I could cope with not adding .done now. My fear is that not having it from day one will make it a lot harder to teach people to use promises (in turn leading to more fear and hatred of them, which there is already far too much of). I'm also concerned that most people are not saying "We can add this later, for now lets just get something out the door" they're saying "This won't be needed once the development tools have caught up, lets never add it." I feel like there might not be another opportunity as good as the current one when it comes to getting this implemented correctly.

erights commented 10 years ago

Please see https://mail.mozilla.org/pipermail/es-discuss/2013-August/032486.html I take the purpose of this repository to be guidance for item 2:

2) What should DOM do quickly [with] tc39's quick output?

which refers back to

1) What should tc39 do quickly, to unblock the DOM's need for promises and avoid a design fork?

At the upcoming tc39 meeting, I will thus hope to see tc39 adopt a compatible superset (item 1) of the draft standard specified by this repository, enabling DOM to adopt the draft standard specified by this repository itself as the subset demanded by their immediate needs (item 2).

The superset I will advocate will include .flatMap and .of, per the AP2 agreement. And it will include .done. Both because I already thought .done would be necessary, and because I find the arguments in this thread to be compelling new reasons for wanting .done. However I still don't think .done belongs in item 2, and so not in this repository.

As for item 3, we will be able to decide based on experience we don't yet have. This is good.