Promises as Cancelation Tokens

# Kevin Smith (5 years ago)

I'm interested in exploring the idea of using an approach similar to .NET's cancelation tokens in JS for async task cancelation. Since the cancelation "flag" is effectively an eventual value, it seems like promises are well-suited to modeling the token. Using a promise for a cancelation token would have the added benefit that the eventual result of any arbitrary async operation could be used as a cancelation token.

First, has this idea been fully explored somewhere already? We've discussed this idea on es-discuss in the past, but I don't remember any in-depth analysis.

Second, it occurs to me that the current promise API isn't quite ideal for cancelation tokens, since we don't have synchronous inspection capabilities. For example, suppose that we have this async function:

async function f(cancel) {
  let canceled = false;
  cancel.then(_=> canceled = true);
  await cheapOperation(cancel);
  if (canceled) throw new CanceledOperationError();
  await expensiveOperation(cancel);
}

Now, when the canceled flag is checked before expensiveOperation, it may be the case that the cancel promise has already been resolved (i.e. [[PromiseState]] is "fulfilled"), but the then callback has not executed yet. In such a case expensiveOperation would be started, even though a cancelation has been requested.

For this reason, it seems like some kind of synchronous inspection ability is required in order to use promises as cancelation tokens. With synchronous inspection, the above function would be written something like:

async function f(cancel) {
  await cheapOperation(cancel);
  if (cancel.isFulfilled()) throw new CanceledOperationError();
  await expensiveOperation(cancel);
}

Without synchronous inspection, I think we'd need to introduce some new API for cancelation tokens.

Thoughts? (For the purposes of this discussion, let's avoid discussing the relative merits of cancelable promises vs. cancelation tokens.)

# Domenic Denicola (5 years ago)

In general I agree that there is a nice conceptual symmetry, but IMO the day-to-day impedance mismatch would be simply too great. Compare:

async function f(cancelationToken) {
  cancelationToken.then(() => console.log("FYI you have been canceled"));

  await cheapOperation(cancelationToken);
  if (cancelationToken.state === "fulfilled") {
    throw new CanceledOperationError();
  }
  await expensiveOperation(cancelationToken);
}

const token = new Promise(resolve => {
  cancelButton.onclick = resolve;
});

f(token);

to the same thing with different names:

async function f(cancelationToken) {
  cancelationToken.whenCanceled(() => console.log("FYI you have been canceled"));

  await cheapOperation(cancelationToken);
  if (cancelationToken.canceled) {
    throw new CanceledOperationError();
  }
  await expensiveOperation(cancelationToken);
}

const token = new CancelationToken(cancel => {
  cancelButton.onclick = cancel;
});

f(token);

I am also unsure when .whenCanceled is necessary (maybe for interfacing with cancelable systems that are not cancelation token aware, like XHR?). And it seems likely that the rejection path is not necessary.

So, if cancelation tokens are the right path, I'd prefer not reusing promises for them. Maybe it argues for easily being able to create a cancelation token from a promise, though.

Semi-related: I was recently reading joeduffyblog.com/2015/11/19/asynchronous-everything, which has a section on Cancelation.

# Bradley Meck (5 years ago)

I agree that without synchronous inspection cancellation tokens become very hard to deal with as you may be queued for a long time prior to cancellation on the event loop, and then the operation is cancelled while you are waiting to check for cancellation. Is there a reason to use a Promise as the cancellation token, rather than have something that is synchronously inspectable? Even a small class/interface like:

let cancelled = false;
myPromise.then(() => cancelled = true);

task[Symbol.cancelled] = () => cancelled;

For this reason I do not think Promises are the right data type to handle Cancellation. There are several existing uses of cancellation tokens that are just {[nameForIsCancelledMethod](): {/*return boolean here*/}}.

There are several reasons I don't think Promises should be extended any further, but that causes a divide amongst people wanting them to represent all possible async tasks and me.

I am also unsure when .whenCanceled is necessary (maybe for interfacing with cancelable systems that are not cancelation token aware, like XHR?). And it seems likely that the rejection path is not necessary.

I don't think so, but you do need to create a way to allow canceling the task from inside of another task (can be done by having a data structure that is passed in to your task runner). I had to do that while working with Node's .pipe in bmeck/managed-task/blob/master/lib/examples/lockfile.js#L66

I know VSCode has that event, maybe they can shed light on why?

# Kevin Smith (5 years ago)

Is there a reason to use a Promise as the cancellation token, rather than have something that is synchronously inspectable?

The only real downside of coming up with a new interface is that we have to standardize it. : ) It's a core protocol.

I agree that using a promise directly would feel awkward without a helper library. You'd probably also want a method that would throw an error if the cancel token was activated (as in .NET msdn.microsoft.com/en-us/library/system.threading.cancellationtoken.throwifcancellationrequested(v=vs.110).aspx ):

cancelToken.throwIfCanceled();

Instead of the more verbose:

if (cancelToken.canceled)
  throw new OperationCancelledError();

I also like the revealing constructor pattern that Domenic mentioned. It's almost good enough, as-is, for converting from a promise to a cancelation token:

new CancellationToken(cancel => somePromise.then(cancel));

Nice!

# Kevin Smith (5 years ago)

And what's the deal, is it canceled or cancelled? : )

# Domenic Denicola (5 years ago)
# Kevin Smith (5 years ago)

I am also unsure when .whenCanceled is necessary

Maybe in the case where you have a promise-returning function and you want to reject the returned promise upon cancellation.

function delayWithCancel(ms, cancelToken) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, ms);
    cancelToken.whenCancelled(reject);
  });
}
# Tab Atkins Jr. (5 years ago)

On Mon, Jan 4, 2016 at 9:01 AM, Domenic Denicola <d at domenic.me> wrote:

From: Kevin Smith [mailto:zenparsing at gmail.com]

And what's the deal, is it canceled or cancelled? : )

This is kind of the worst. Previous discussion at promises-aplus/cancellation-spec#4.

Data seems to favor cancelled:

The best approach in cases like this is to avoid the word altogether. The fact that there's confusion at all means people will mess it up and get annoyed, even if there's a "winner" in overall usage.

On Mon, Jan 4, 2016 at 9:36 AM, Kevin Smith <zenparsing at gmail.com> wrote:

I am also unsure when .whenCanceled is necessary

Maybe in the case where you have a promise-returning function and you want to reject the returned promise upon cancellation.

function delayWithCancel(ms, cancelToken) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, ms);
    cancelToken.whenCancelled(reject);
  });
}

Yes, forcing people to poll an attribute of a Promise-like thing is kinda ridic. ^_^

# Kevin Smith (5 years ago)

The best approach in cases like this is to avoid the word altogether. The fact that there's confusion at all means people will mess it up and get annoyed, even if there's a "winner" in overall usage.

Hmmm... Maybe

class CancelToken {
  constructor(init);
  get cancelRequested() : bool;
  whenCancelRequested(callback) : void;
}

Or more/overly tersely:

class CancelToken {
  constructor(init);
  get requested() : bool;
  whenRequested(callback) : void;
}
# Benjamin Gruenbaum (5 years ago)

We have pretty sound cancellation semantics in bluebird 3.

bluebirdjs.com/docs/api/cancellation.html

Handles multiple subscribers soundly. Solves the common use cases pretty well - has absolutely zero magic and pretty simple semantics. They work with .all and .race too.

We have had a lot of positive feedback regarding the change and it works well (at least in my code) with async/await and other newer proposals.

Biggest downside is that it hasn't been used for very long yet in production (<year) - it often takes longer to discover problems.

It would be great if the TC and the list took a look at the bluebird cancellation semantics - they took years to nail.

So here I am bringing them to your attention. I'll probably stay out of arguments but feel free to ask any questions if you'd like.

# Kevin Smith (5 years ago)

We have pretty sound cancellation semantics in bluebird 3.

Cool, I'm aware that cancellable promises have been explored in depth. I'd prefer to keep this thread focused on cancellation tokens though, and avoid comparisons.

# /#!/JoePea (5 years ago)

Since checking promise.state is synchronous, we may as well just write a synchronous Cancel class instead:

class CancelError extends Error { /* ... */ }

class Cancel {
    constructor() { this.requested = false }
    request() { this.requested = true }
    throw() { throw new CancelError() }
}

async function f(cancel) {
    await cheapOperation(cancel) // some other function might call
cancel.request() while we await here.
    if (!cancel.requested)
        await expensiveOperation(cancel)
}

let cancel = new Cancel
cancelButton.onclick = () => cancel.request()

f(cancel)

Wouldn't this be better than using a Promise if we are relying on a synchronous check anyways?

# Benjamin Gruenbaum (5 years ago)

Cool, I'm aware that cancellable promises have been explored in depth.

I'd prefer to keep this thread focused on cancellation tokens though, and avoid comparisons.

Cool, re-reading the discussion you asked for this in the first message - I apologize for missing it.

I think using promises as tokens would be problematic. It would have several issues:

  • You'd want to be able to cancel things like Promise.all and Promise.race - with a separate type you can use "the last argument", with a promise you would not be able.
  • The names would get confusing for the users. Like Domenic said - whenCancelled sounds a lot clearer. Sort of like the function vs. object discussion on es-observable.
  • You get some weird scenarios like cancelling a cancellation.
  • You'd probably need to add properties anyway so that tokens can be aggregated.

I do definitely see the use for whenCancelled and have used its equivalent myself in C#.

It's also important to keep in mind that promises can be subclassed so it's fine to add properties to them if used for a specific purpose like cancellation.

# Kevin Smith (5 years ago)

Since checking promise.state is synchronous, we may as well just write a synchronous Cancel class instead:

Right - see upthread. You do need some kind of callback method, though, like whenCancelled(callback).

class Cancel { constructor() { this.requested = false } request() { this.requested = true } throw() { throw new CancelError() } }

We need to separate the capability to "read" the cancellation request from the ability to request the cancellation. That's why in .NET you have CancellationTokenSource and CancellationToken. But for JS, we should probably use the revealing constructor pattern instead (discussed upthread):

let token = new CancelToken(cancel => { ... });
# /#!/JoePea (5 years ago)

Cool, yeah, the reveal pattern would indeed be a good way to guard against unwanted/unexpected cancels.

class Cancel {
    constructor(executor) {
        this._requested = false
        executor(() => this._requested = true)
    }
    get requested() {return this._requested}
    throw() { throw new CancelError() }
}

let cancel = new Cancel(function(cancel) {
    cancelButton.onclick = cancel
})

What would be the recommended way of keeping the internal state private? With a WeakMap?

# Kevin Smith (5 years ago)
throw() { throw new CancelError() }

This should be throwIfRequested I think, e.g.

throwIfRequested() {
  if (this._requested)
    throw new CancelError();
  }
}

What would be the recommended way of keeping the internal state private? With a WeakMap?

Spec-defined objects can have "internal slots" (essentially private fields), which can be simulated with WeakMaps. Although a polyfill would most likely just use underscored names.

# Kevin Smith (5 years ago)

I think using promises as tokens would be problematic. It would have several issues:

Agreed with all of those.

It's also important to keep in mind that promises can be subclassed so it's

fine to add properties to them if used for a specific purpose like cancellation.

We could use a promise subclass as the cancellation token, but then tokens (and their constructor) would inherit things that really don't make sense, like "CancelToken.resolve" and "CancelToken.prototype.catch".

On the other hand, it doesn't really make sense to create a new callback API (whenCancelled), when we already have the necessary callback semantics with promises. Perhaps we can expose a promise on the token instead:

class CancelToken {
    get requested() : bool;
    get promise() : Promise;
    throwIfRequested() : void;
}

// Registering a callback
token.promise.then(_=> doSomething());

// Doing other things with the promise
Promise.race(token.promise, someOtherPromise);
# Benjamin Gruenbaum (5 years ago)

We could use a promise subclass as the cancellation token, but then

tokens (and their constructor) would inherit things that really don't make sense, like "CancelToken.resolve" and "CancelToken.prototype.catch".

Generally I dislike inheritance. I was merely saying it's an option. I favor composition here as well.

# Ron Buckton (5 years ago)

This gistgist.github.com/rbuckton/b69be537e41feec7fea7 contains the TypeScript declarations for the version of CancellationToken I’ve been using for a number of small projects. The basic API shape is:

class CancellationTokenSource {
  constructor(linkedTokens?: CancellationToken[]);
  token: CancellationToken;
  cancel(reason?: any): void;
  close(): void;
}
class CancellationToken {
  static none: CancellationToken;
  canBeCanceled: boolean;
  canceled: boolean;
  reason: any;
  throwIfCanceled(reason?: any): void;
  register(callback: (reason?: any) => void): CancellationTokenRegistration;
}
interface CancellationTokenRegistration {
  unregister(): void;
}

The approach I use has the following considerations:

• The responsibility for cancellation is split between two types:

o CancellationTokenSource – This type is responsible for propagating a cancellation signal and linking multiple tokens together (useful to cancel a graph of async operations, such as cancelling all pending fetch request).

o CancellationToken – A read-only sink for a cancellation signal. Asynchronous operations can either synchronously observe whether the cancellation signal has been set, or register an callback for asynchronous notification of a cancellation signal.

• Once a callback has been registered for asynchronous notification of a cancellation signal, it can later be unregistered.

• Asynchronous notifications are queued and handled at a higher priority than Promise continuations.

• The reason for cancellation can be explicitly supplied. If not supplied, the reason would be a generic “The operation was canceled” error.

Splitting a cancellation signal across two classes has several benefits. One benefit is that it allows the producer of a cancellation signal to limit access to the ability to initiate that signal. This is important if you wish to have a single shared cancellation source for multiple concurrent asynchronous operations, but need to hand off a cancellation token to a third party without worrying that the third party code would unintentionally cancel operations that it does not itself own. Another benefit is the ability to create more complex cancellation graphs through linking multiple cancellation tokens.

I agree with Domenic that it is necessary to be able to synchronously observe the canceled state of a cancellation token, and that it is also necessary to be able to observe a cancellation signal asynchronously. However, I have found that the asynchronous notification needs to be triggered at a higher priority than Promise “then” tasks. Consider the following, albeit contrived, example:

let source = new CancellationTokenSource();
let p = new Promise((resolve, reject) => {
  Promise.resolve(1).then(resolve);
  source.token.register(reject);
  source.cancel();
});

Since “source.cancel()” is executed synchronously, one would expect that p would be rejected. If the notification were merely added to the same micro-task queue as Promise “then” tasks, p would be resolved first. By making the micro-task queue into a priority queue, we can allow cancellation notifications to be processed at a higher priority while still keeping the API itself asynchronous. In .NET, cancellation notifications are executed synchronously, however this conflicts with the intent to ensure an asynchronous API is consistently asynchronous.

This approach also allows cancellation tokens to be more general-purpose. By not directly tying cancellation into Promises, they can be used with other future asynchronous primitives such as Observables, async generators, etc.

Ron

From: Kevin Smith<mailto:zenparsing at gmail.com>

Sent: Monday, January 4, 2016 11:44 AM To: Tab Atkins Jr.<mailto:jackalmage at gmail.com>; Domenic Denicola<mailto:d at domenic.me>

Cc: es-discuss<mailto:es-discuss at mozilla.org>

Subject: Re: Promises as Cancelation Tokens

The best approach in cases like this is to avoid the word altogether. The fact that there's confusion at all means people will mess it up and get annoyed, even if there's a "winner" in overall usage.

Hmmm... Maybe

class CancelToken {
  constructor(init);
  get cancelRequested() : bool;
  whenCancelRequested(callback) : void;
}

Or more/overly tersely:

class CancelToken {
  constructor(init);
  get requested() : bool;
  whenRequested(callback) : void;
}
# Dean Tribble (5 years ago)

From experience, I'm very much in favor of the cancellation token. Though

promises provide a good inspiration for cancellation, they don't quite fit the problem directly.

The approach has been explored, though the details are not published. I implemented cancellation for the Midori system. Midori was an incubation project to build a new OS, applications, etc. in a safe language (a C# variant) using lightweight processes communicating via promises (easily 2M+ lines of C#). Among other things, it strongly influenced the C# async support. See joeduffyblog.com/2015/11/19/asynchronous-everything for more info. After looking at various options, the approach developed for .Net covered all the requirements we had and could be adapted surprisingly well to the async world of promises and Midori. There were some important differences though.

In a concurrent, async, or distributed system, cancellation is *necessarily *asynchronous: there's fundamentally a race between a computation "expanding" (spawning more work) and cancellation running it down. But as you note, a cancellation system must support the ability to *synchronously *and cheaply test whether a computation is being cancelled. That allows one for example to write a loop over thousands of records that will abort quickly when cancelled without scheduling activity for the remaining records. (Because of the inherent race in cancellation, I named that "isCancelling" rather than "isCancelled" to be a small reminder that computation elsewhere might not yet have heard that it should stop.)

In async cancellation, the promise "then" seems like it could support the asycn "cleanup" action on cancellation. However there are a lot of cancellation use cases in which the appropriate cleanup action changes as a computation proceeds. For those, using "then" is not adequate. For example, a browser would have a cancellation token associated with a page load. Thus the same token is used during parsing, retrieving secondary images, layout, etc. If the user hits "stop", the token is cancelled, and so all the various heterogeneous page rendering activities are cancelled. But the cleanup action to "close the socket that you are retrieving an image over" becomes expensive deadweight once the image has been retrieved. On a page that loads 100 images four at a time, you would want 4 cleanup actions registered, not 100.

For that and other reasons, we found it much clearer to give cancellationToken it's own type. That also allows convenient patterns to be directly supported, such as:

async function f(cancel) {
  await cheapOperation(cancel);
  cancel.*throwIfCancelling*(); // throws if the token is cancelling
  await expensiveOperation(cancel);
}

Note that the above would more likely have been written simply:

async function f(cancel) {
  await cheapOperation(cancel);
  await expensiveOperation(cancel);
}

The reason is that if the cheapOperation was aborted, the first await would throw (assuming cheapOperation terminates abruptly or returns a broken promise). If it got past cheapOperation, we already know that expensiveOperation is going to be smart enough to cancel, so why clutter our world with redundant aborts? e.g.,

async function expensiveOperation(cancel) {
  while (hasFramesToRender() && !cancel.isCancelling()) {
      await renderFrame(this.nextFrame(), cancel);
  }
}

Thus, using cancellation tokens becomes simpler as cancellation becomes more pervasive in the libraries. Typically, if an operation takes a cancellation token as an argument, then you don't need to bother protecting it from cancellation. As a result, explicit cancellation handling tends to only be needed in lower level library implementation, and client code passes their available token to either operations or to the creation of objects (e.g., pass in your token when you open a file rather than on every file operation).

# Kevin Smith (5 years ago)

Thanks Ron! Comments inline...

· Once a callback has been registered for asynchronous notification of a cancellation signal, it can later be unregistered.

Yes, I see how this could be helpful.

· Asynchronous notifications are queued and handled at a higher priority than Promise continuations.

· The reason for cancellation can be explicitly supplied. If not supplied, the reason would be a generic “The operation was canceled” error.

What's the benefit of allowing a user-supplied error? (I assume that by convention the reason should be an error type.) I don't see that feature in .NET.

Since “source.cancel()” is executed synchronously, one would expect that p would be rejected. If the notification were merely added to the same micro-task queue as Promise “then” tasks, p would be resolved first.

I agree that your example, as written, is counter-intuitive. However, is it still counter-intuitive if I rewrite it like this?

// Suppose I have "cancelToken" and "cancel"
let p = new Promise((resolve, reject) => {
  Promise.resolve(1).then(resolve);
  cancelToken.promise.then(reject);
  cancel();
});

Written like this, it seems to me that the ordering is as expected. Are there use cases that require a higher priority, beyond user expectation based on the API?

In .NET, cancellation notifications are executed synchronously, however

this conflicts with the intent to ensure an asynchronous API is consistently asynchronous.

Right. The trouble I have with introducing an ad-hoc callback API for cancel tokens centers mostly around the handling and propagation of errors. In .NET, exceptions which occur inside of callbacks are propagated synchronously to the caller via an AggregateException error collection. But if the callbacks are truly executed asynchronously, where do the errors go? Do they propagate out to the host, like HTML event handlers? It would be unfortunate if no mechanism was provided by ECMAScript itself to handle such errors.

Since we already have a well-defined and well-understood way to propagate errors using promises, I would prefer to have the async registration capability modeled as a promise, if possible. There is that "unregister" issue, though...

# Kevin Smith (5 years ago)

Thanks for posting this. Great stuff!

On a page that loads 100 images four at a time, you would want 4 cleanup actions registered, not 100.

And in order to keep it to 4 you need a way to unregister the action when you complete the operation, which the promise API doesn't give you. I see.

Conceptually, you could of course set some "complete" flag when you finish the operation and then have the cleanup action early-exit if that flag is set, but that would be unwieldy. And it wouldn't stop the unbounded growth of the promise's handler list.

Interesting!

# Katelyn Gadd (5 years ago)

One key thing to recognize is that there are different reasons to cancel an operation and as a result, different approaches to cancellation. .NET cancellation tokens address one scenario (and do it fairly well), where the objective is for specific operations to expose cancellation and allow the caller to set up a way to cancel the operation once it is in progress. This requires co-operation on both sides but is very easy to debug and reason about.

However, this does not address all cancellation scenarios.

Another cancellation scenario is when the consumer of an asynchronous task no longer needs the result of an operation. In this case, they will only have access to the Promise unless the cancellation token is routed to them through some other path. This becomes especially unwieldy if you are dealing with a graph of asynchronous operations, where you want this 'no longer needed' property to propagate through all of the promises. This is somewhat equivalent to how you want a garbage collector to collect a whole tree of objects once they are no longer reachable. In JS (and in my opinion, basically all language environments) you want this to be explicit even if the GC is able to lazily flag the result of a promise as no longer needed. In the 'result no longer needed' scenario there are also cases where you do not want to cancel the operation even if it is not needed anymore.

A third form of cancellation is already addressed by Promise implementations - error handling. In this case, an error occurs and the whole asynchronous process (usually) needs to be aborted and unwound so that the error can be responded to. In this scenario cancellation can occur at any time and in some cases it is not possible for the application to continue correctly under these circumstances. You could use exceptions to implement the other forms of cancellation, but it's pretty unwieldy - and injecting exceptions into code that doesn't expect that is a generally bad policy. .NET used to allow injecting exceptions into other threads but has since deprecated it because of all the nasty corner cases it introduces.

In my opinion cancellation tokens are a great model that does not require any browser vendor/VM implementer support, but it can be beneficial for implementations of specific operations (i.e. XHR) to provide some sort of cancellation mechanism. Essentially, the XHR object acts as the cancellation token and you call a method on it to cancel. In most cases you can implement that in user space.

'Result no longer needed' requires some amount of support at the language/base framework level, so that it is possible to flag this state on any given Promise and it is possible for all Promise consumers to appropriately propagate this state through graphs of asynchronous dependencies. For example, cancelling a HTML pageload should flag any dependent image/script loads as 'no longer needed', but not necessarily abort them (aborting might kill the keepalive connection, and allowing the request to complete might helpfully prime the cache with those resources).

# Benjamin Gruenbaum (5 years ago)

Another cancellation scenario is when the consumer of an asynchronous

task no longer

needs the result of an operation. In this case, they will only have

access to the Promise

unless the cancellation token is routed to them through some other path.

For what it's worth - this is exactly how promise cancellation is modeled in bluebird 3 and we have found it very nice.

I think the distinction between cancellation because of disinterest and cancellation because we actively want to cancel the operation is very important - props for nailing it.

That said - I think most scenarios that work with tokens work without them and vice versa.

I think F#'s cancellation approach is also worth mentioning in the discussion of alternatives as it has implicit but token-based automatically propagating cancellation.

# Kevin Smith (5 years ago)

I think F#'s cancellation approach is also worth mentioning in the discussion of alternatives as it has implicit but token-based automatically propagating cancellation.

If you have any good links to reference materials on F#'s cancellation architecture, feel free to include them for future reference. I was unable to find anything that clearly explained how implicit cancellation tokens are discovered by child tasks, for instance.

I find implicit cancellation to be somewhat sketchy. For async functions, it would mean that any await expression could potentially throw a CancelError:

async function f() {
  let a = await 1; // This could throw?  What?
}

And there are certainly going to be scenarios where you don't want to cancel a subtask. It seems to me that cancellation semantics are best left to the async operation itself, rather than assuming "crash-and-burn".

# Bradley Meck (5 years ago)

For async functions, it would mean that any await expression could potentially throw a CancelError

Cancellation does not necessarily need to use throw, return is often more apt. I find.

I would also recommend splitting the idea of cancellation into: abort semantics, and ignorance semantics. Trying to use one term for both separately is getting confusing.

My usage of cancellation is often being able to undo side-effects which can happily be done using finally.

# Katelyn Gadd (5 years ago)

Implicit cancellation doesn't make sense if it involves a throw. Furthermore, implicit cancellation would never happen for your example

  • the 'await' clearly depends on the result of the operation, so it is in use and it would not make sense for it to be implicitly cancelled. For the record, every time I've successfully dealt with implicit cancellation, it's opt-in - the library author implementing the asynchronous logic decides whether or not it will respond to implicit cancellation.

Explicit cancellation ('abort', to use the terminology split Bradley advocated above) would potentially introduce a throw there, but that should be unsurprising, just as a socket being closed due to an ethernet cable getting unplugged would cause a socket operation to throw "unexpectedly".

There are some subtle distinctions here that APIs tend to get wrong, of course. An example that might be informative:

If you look at sockets, various APIs tend to have differences in how you deal with lifetime & closing handles. In some cases there are subtly different forms operations - for example if you look at Socket in the .NET framework, it exposes three different/overlapping forms of lifetime management, through the Shutdown, Close and Dispose methods. If you compare this with Rust's sockets API, the only explicit operation it exposes is 'shutdown', and the other two are expressed by dropping the value.

For the average .NET user, you can treat all three as equivalent. For casual use cases you can just Dispose all IDisposable resources (files, sockets, graphics resources...) at the end of a function and be ready to go. But behaviorally they are different, and semantically they are different. A socket Shutdown followed by a Dispose will flush the read/write buffers (which can take time) and allow you to cleanly tear everything down, while a standalone Dispose is an instantaneous destruction of the socket, potentially breaking a transmission in the middle. In practice, this distinction represents that the socket is actually a group of related resources - read/write buffers, a network connection, etc - that can't simply be treated as a single unit with consistent lifetime.

Oh yeah, and there's also the detail that you can shut down the write end of a socket but not the read end. :-)

If you map this over to the browser, you can trivially come up with equivalent examples. When I issue XHR requests, some of those requests are more important than others. For example, an XHR that is saving important changes to an email draft should not be aborted if the user clicks on a hyperlink to load a new email. However, an XHR that is downloading an inline image for an email can be safely aborted at a moment's notice. You can think of this as equivalent to Shutdown/Dispose - you would want to Shutdown the draft save operation, flushing everything out, before closing the socket, and that takes time. In comparison, the image load is implicitly cancelled as soon as the image content is no longer needed, and that can terminate the underlying request if appropriate.

# Benjamin Gruenbaum (5 years ago)

F# cancellation - on second thought implicit cancellation through cancellation like in F# is impractical because of the eagerness of promises. I don't think it's a valid alternative here. I've discussed this with Reed Copsey (an F# expert) and he explained the philosophy behind it to me - it doesn't sound practical for JavaScript promises at all.

*Splitting into abort and ignore semantics - *that sounds like a great idea, I think that ignore semantics are far more common for the use cases I run into by the way. I think it might be a good idea to fire this off in a separate ES-Discuss thread.

*Throwing and implicit cancellation *- I agree with Katelyn, throwing and ignore cancellation are exclusive. As Bradley said - return (as in generator .return) is a better mental model. finally blocks are run but not catch blocks. In bluebird 3, cancellation is done with ignore semantics - and we don't throw on cancellation (link again for comparison: bluebirdjs.com/docs/api/cancellation.html ).

*Implicit cancellation being opt in - *I agree too, the nice property is that cancellation becomes an optimization library authors get to make but no other code breaks. A library can add support for freeing requests earlier and you don't have to change your code in order to get the optimization.

*Socket Lifetime Example - *In bluebird we map that with disposers (ref: bluebirdjs.com/docs/api/promise.using.html ), we have found that in the last few years although the library gets millions of downloads very few people actually use using. It is an interesting motivating example and I think we should collect those for any future proposal.

*XHR Example *- I think that ignore semantics can model that. The XHR saving important changes would simply not be cancellable under implicit cancellation - trying to cancel it would result in a no-op.