cwmma gets a lot right here: Promises have always been contentious in JS (and TC39) and Domenic has indeed had the patience of a saint attempting to wrangle the various points of view into a coherent proposal.
TC39 as a group is generally very motivated to find positive-sum outcomes and find solutions that address everyone's constraints in a satisfactory way. That doesn't usually mean design-by-committee: champions work on a coherent design that they feel hangs together, and the committee provides feedback on constraints, not solutions.
As a member of TC39, I'm usually representing ergonomic concerns and the small-company JavaScript developer perspective. I've had a lot of luck, over the years, in giving champions my perspective and letting them come back with an improved proposal.
The staging process (which I started sketching out on my blog[1]) has made the back-and-forth easier, which each stage representing further consensus that the constraints of individual members have been incorporated.
Unfortunately, I fear that promise cancellation may be a rare design problem with some zero-sum questions.
It's worth noting that there has been no objection, on the committee, to adding cancellation to the spec in some form.
The key questions have been:
First. Is cancellation a normal rejection (a regular exception, like from `throw`) or a new kind of abrupt completion (which `finally` would see but not `catch`). The current status quo on the committee, I believe, is that multiple people would have liked to see "third-state" (as Domenic called it) work, but the compatibility issues with it appear fatal.
Second. Should promises themselves be cancelled (`promise.cancel()`) or should there be some kind of capability token threaded through promises.
What that would look like:
let [sendCancel, recvCancel] = CancelToken.pair();
fetchPerson(person.id, recvCancel);
async function fetchPerson(id, token) {
// assume fetch is retrofitted with cancel token support
let person = await fetch(`/users/${id}`, { token });
}
// when the cancel button is clicked, cancel the fetch
cancelButton.onclick = sendCancel;
This approach had many supporters in the committee, largely because a number of committee members have rejected the idea of `promise.cancel()` (in part because of ideological reasons about giving promise consumers the power to affect other promise consumers, in part because of a problem[2] Domenic raised early about the timing of cancellation, and in part because C# uses cancel tokens[3]).
In practice, this would mean that intermediate async functions would need to thread through cancel tokens, which is something that bothered me a lot.
For example, it would have this affect on Ember, if we wanted to adopt cancel tokens:
In other words, any async hook (or callback) would need to manually thread tokens through. In Ember, we'd like to be able to cancel async tasks that were initiated for a previous screen or for a part of the screen that the user has navigated away from.
In this case, if the user forgot to take the cancel token (which would likely happen all the time in practice), we would simply have no way to cancel the ongoing async.
We noticed this problem when designing ember-concurrency[4] (by the venerable Alex Matchneer), and chose to use generators instead, which are more flexible than async functions, and can be cancelled from the outside.
At last week's Ember Face to Face, we discussed this problem, and decided that the ergonomic problems with using cancel tokens in hooks were sufficiently bad that we are unlikely to use async functions for Ember hooks if cancellation requires manually propagating cancel tokens. Instead, we'd do this:
The `*` is a little more cryptic, but it's actually shorter than `async`, and doesn't require people to thread cancel token through APIs.
Also notable: because JavaScript doesn't have overloading (unlike C#), it is difficult to establish a convention for where to put the cancel token ("last parameter", vs. "the name `token` in the last parameter as an options bag" vs. "first parameter"). Because cancellation needs to be retrofitted onto a number of existing promise-producing APIs, no one solution works. This makes creating general purpose libraries that work with "promise-producing functions that can be cancelled" almost impossible.
The last bit (since I started talking about Ember) is my personal opinion on cancel tokens. On the flip side, a number of people on the committee have a very strongly held belief that cancel tokens are the only way to avoid leaking powerful capabilities to promise consumers.
A third option, making a new Task subclass of Promise that would have added cancellation capabilities, was rejected early on the grounds that it would bifurcate the ecosystem and just mean that everyone had to use Task instead of Promise. I personally think we rejected that option too early. It may be the case that Task is the right general-purpose answer, but people with concerns about leaking capabilities to multiple consumers should cast their Tasks to Promises before passing them around.
As I said, I think this may be a (very, very) rare case where a positive-sum outcome is impossible, and where we need, as a committee, to discuss what options are available that would minimize the costs of making a particular decision. Unfortunately, we're not there yet.
Domenic has done a great job herding the perspective cats here, and I found his presentations on this topic always enlightening. I hope the committee can be honest enough about the competing goals in the problem of cancellation so that Domenic will feel comfortable participating again on this topic.
Thank you for this response. It's very well thought out and nuanced. I'm not sure what the OP had to deal with, specifically, but I share the same opinions as yourself.
Promises themselves are generic async handling work horses. Cancelling a HTTP call makes sense. Does it makes sense to cancel an operation currently happening in, say, a loop? Likely not.
Cancellable Promises are a reality in our software stack for years in Liferay and related open source projects. JavaScript is a flexible language and this is what makes it what it is, fits into different realities and skill levels. Optionally, you can use Object.defineProperty() to define how your object behaves, if it's configurable, enumerable, writable and so on. You can Object.freeze() to freeze an object. Having a Promise that the consumer can define whether or not it can be cancelled makes sense in general. There are lots of use-cases that the consumer can safely decide if canceling a promise or chaining of promises is problematic or not, e.g. ajax calls, async life-cycle of a UI component.
There are many good arguments about cancellation tokens vs promise.cancel() on ES Discuss https://esdiscuss.org/topic/cancellation-architectural-obser.... One argument that is not accurate is that "cancellation is heterogeneous it can be misleading to think about canceling a single activity". In most systems, it's implemented expecting that async operations can be cancelled, intentionally or not (network problems for example). There is no single answer for this problem, it can be wrong and dangerous, or it can be safe and predictable, that all depends on how the consumer utilizes it.
Domenic mentioned in the proposal https://github.com/tc39/proposal-cancelable-promises/issues/... that most of Googlers on TC39 are against cancellable promises, on the other hand Google Closure Library has a very robust implementation brought from labs in 2014 by Nanaze https://github.com/google/closure-library/commit/74b27adf7. It's heavily used on large real world async applications, such as Gmail, G+, Docs. It shows clearly that there is a real space for cancelling promises without being dangerous.
Thank you! This is helpful.
Do you believe there is no positive sum because Promises are out there already, and this could have been avoided had they been released with cancellation in the first place?
I think it's very likely that we could avoid turning this into a zero-sum debate with one total-winner and one total-loser.
That said, I think there's no positive sum because some folks believe that (1) Promises should be the primary async story in JS, and (2) Promises must not allow communication between two parties who hold a reference to the Promise. This means that `async function`s must return a Promise, and the Promise but not have a `cancel()` method on it (because it would allow communication between two parties holding references to the Promise).
Others (I'll speak for myself here) believe that the return value of `async function` should be competitive (ergonomically) with generators used as tasks (I showed examples in the parent comment). Since generators-as-tasks can be cancelled (via `.return()` and `.throw()`), the desire to make `async function x` as ergonomic as `function* x` conflicts with the goal of disallowing the return value of async functions from being cancellable.
In Ember's case, since generators already exist, it's hard for us to justify asking application developers to participate in an error-prone (and verbose) protocol that we could avoid by using generators-as-tasks instead. And that is likely the conclusion that we will ship (and the conclusion that ember-concurrency has already shipped).
For me, the bottom line is that we have very little budget to introduce new restrictions on `async functions`, because people can always choose to reject async functions and use generators instead (with more capabilities and less typing!). I think the cancellation token proposal is well over-budget from that perspective.
In my opinion, having a separate cancellation token that is "threaded through" the call stack is indisposable, because sometimes you need to do more than simply pass along the token that you've got - usually it involves linking several tokens together, e.g. with another representing the state of the component as a whole, as opposed to that particular call chain.
Now, this doesn't happen often, and most of the time you do indeed end up just passing the token along. But when you do need it, it allows for code drastically simpler than any workarounds.
My takeaway from this is that first-class cancellation tokens are the right approach, but languages need some kind of syntactic sugar to eliminate, or at least reduce, the verbiage for the most common case of propagating it around.
(All of this is based on experience working with a heavily async/await codebase written in C# for the past few years.)
> My takeaway from this is that first-class cancellation tokens are the right approach, but languages need some kind of syntactic sugar to eliminate, or at least reduce, the verbiage for the most common case of propagating it around.
That is also my perspective, but I think the syntactic sugar cannot have much more overhead than `async function`.
The good thing is that it's also a pattern that is highly amenable to syntactic sugar, simply because of how regular it is.
Actually, come to think of it, it is a particular instance of a more common pattern where you receive some state, and propagate it to most (usually, all) further calls that expect it. Scala implicit parameters and variables cover this in the most generic way.
cwmma gets a lot right here: Promises have always been contentious in JS (and TC39) and Domenic has indeed had the patience of a saint attempting to wrangle the various points of view into a coherent proposal.
TC39 as a group is generally very motivated to find positive-sum outcomes and find solutions that address everyone's constraints in a satisfactory way. That doesn't usually mean design-by-committee: champions work on a coherent design that they feel hangs together, and the committee provides feedback on constraints, not solutions.
As a member of TC39, I'm usually representing ergonomic concerns and the small-company JavaScript developer perspective. I've had a lot of luck, over the years, in giving champions my perspective and letting them come back with an improved proposal.
The staging process (which I started sketching out on my blog[1]) has made the back-and-forth easier, which each stage representing further consensus that the constraints of individual members have been incorporated.
Unfortunately, I fear that promise cancellation may be a rare design problem with some zero-sum questions.
It's worth noting that there has been no objection, on the committee, to adding cancellation to the spec in some form.
The key questions have been:
First. Is cancellation a normal rejection (a regular exception, like from `throw`) or a new kind of abrupt completion (which `finally` would see but not `catch`). The current status quo on the committee, I believe, is that multiple people would have liked to see "third-state" (as Domenic called it) work, but the compatibility issues with it appear fatal.
Second. Should promises themselves be cancelled (`promise.cancel()`) or should there be some kind of capability token threaded through promises.
What that would look like:
This approach had many supporters in the committee, largely because a number of committee members have rejected the idea of `promise.cancel()` (in part because of ideological reasons about giving promise consumers the power to affect other promise consumers, in part because of a problem[2] Domenic raised early about the timing of cancellation, and in part because C# uses cancel tokens[3]).In practice, this would mean that intermediate async functions would need to thread through cancel tokens, which is something that bothered me a lot.
For example, it would have this affect on Ember, if we wanted to adopt cancel tokens:
In other words, any async hook (or callback) would need to manually thread tokens through. In Ember, we'd like to be able to cancel async tasks that were initiated for a previous screen or for a part of the screen that the user has navigated away from.In this case, if the user forgot to take the cancel token (which would likely happen all the time in practice), we would simply have no way to cancel the ongoing async.
We noticed this problem when designing ember-concurrency[4] (by the venerable Alex Matchneer), and chose to use generators instead, which are more flexible than async functions, and can be cancelled from the outside.
At last week's Ember Face to Face, we discussed this problem, and decided that the ergonomic problems with using cancel tokens in hooks were sufficiently bad that we are unlikely to use async functions for Ember hooks if cancellation requires manually propagating cancel tokens. Instead, we'd do this:
The `*` is a little more cryptic, but it's actually shorter than `async`, and doesn't require people to thread cancel token through APIs.Also notable: because JavaScript doesn't have overloading (unlike C#), it is difficult to establish a convention for where to put the cancel token ("last parameter", vs. "the name `token` in the last parameter as an options bag" vs. "first parameter"). Because cancellation needs to be retrofitted onto a number of existing promise-producing APIs, no one solution works. This makes creating general purpose libraries that work with "promise-producing functions that can be cancelled" almost impossible.
The last bit (since I started talking about Ember) is my personal opinion on cancel tokens. On the flip side, a number of people on the committee have a very strongly held belief that cancel tokens are the only way to avoid leaking powerful capabilities to promise consumers.
A third option, making a new Task subclass of Promise that would have added cancellation capabilities, was rejected early on the grounds that it would bifurcate the ecosystem and just mean that everyone had to use Task instead of Promise. I personally think we rejected that option too early. It may be the case that Task is the right general-purpose answer, but people with concerns about leaking capabilities to multiple consumers should cast their Tasks to Promises before passing them around.
As I said, I think this may be a (very, very) rare case where a positive-sum outcome is impossible, and where we need, as a committee, to discuss what options are available that would minimize the costs of making a particular decision. Unfortunately, we're not there yet.
Domenic has done a great job herding the perspective cats here, and I found his presentations on this topic always enlightening. I hope the committee can be honest enough about the competing goals in the problem of cancellation so that Domenic will feel comfortable participating again on this topic.
[1]: https://thefeedbackloop.xyz/tc39-a-process-sketch-stages-0-a...
[2]: https://github.com/tc39/proposal-cancelable-promises/issues/...
[3]: https://msdn.microsoft.com/en-us/library/dd997289(v=vs.110)....
[4]: http://ember-concurrency.com/#/docs