Skip to content
This repository was archived by the owner on Feb 2, 2023. It is now read-only.

How to cancel a running async function #27

Closed
mnieper opened this issue Jan 26, 2015 · 83 comments
Closed

How to cancel a running async function #27

mnieper opened this issue Jan 26, 2015 · 83 comments
Labels

Comments

@mnieper
Copy link

mnieper commented Jan 26, 2015

Is it possible to short-circuit a running async function like one can do with a generator by calling its return method?

A use case would be to have a finally block invoked in order to clean up resources, etc.

@bmeck
Copy link
Member

bmeck commented Feb 5, 2015

in a similar vein we have a lot of code that relies on errors coming from outside sources, but I can't find a way to abort a running async function:

async function () {
  // ... get a child process ...

  // caused by some things that do not actually kill the process
  // e.g. stdio causing SIGPIPE
  child_process.on('error', abort);

  // ... do things until .on('exit') ...
}

for a lot of things like streams, where errors are disjoint (not in the same stack as a pending callback) it would be nice to have a clear way to abort.

@steelbrain
Copy link

Throw an error maybe? Instead of aborting.

@getify
Copy link

getify commented Feb 12, 2015

I'd consider this to be a blocking issue in the current design. If the OP case wasn't addressed (aborting from outside), I would never use async functions and would just stick with the slightly uglier generator+promise option with a runner util.

A possible idea to address:

async function foo() { .. }

var ret = foo();
ret.value;       // promise
ret.return(..);  // analogous to generator `return(..)`
                 // returns either its own separate promise
                 // or the original `ret.value` promise

so...

foo().value.then(..) 

// instead of

foo().then(..)

Not wonderful, but certainly workable compared to no solution.

@petkaantonov
Copy link

@getify How does .return() as specified not lead to resource leaks and inconsistent state?

function* () {
    // Set up state and resources
    try {
        yield thing1();
        yield thing2(); // Say you cancel while waiting for this...
        yield thing3();
    } finally {
        // Cleanup
    }
}

If you just cancel that generator with .return() it will leave behind resources and inconsistent state?

@mnieper
Copy link
Author

mnieper commented Feb 20, 2015

The finally block will be executed by .return().

2015-02-19 23:48 GMT+01:00 Petka Antonov [email protected]:

@getify https://github.com/getify How does .return() as specified not
lead to resource leaks and inconsistent state?

function* () {
// Set up state and resources
try {
yield thing1();
yield thing2(); // Say you cancel while waiting for this...
yield thing3();
} finally {
// Cleanup
}
}

If you just cancel that generator with .return() it will leave behind
resources and inconsistent state?


Reply to this email directly or view it on GitHub
#27 (comment)
.

@petkaantonov
Copy link

@mnieper nothing about that in the es6 spec, and v8 doesn't implement it so cannot test

@mnieper
Copy link
Author

mnieper commented Feb 20, 2015

See here

http://people.mozilla.org/~jorendorff/es6-draft.html#sec-generator.prototype.return

which relies on

http://people.mozilla.org/~jorendorff/es6-draft.html#sec-generatorresumeabrupt

which in turn resumes a suspended generator in step 11.

Now look at

http://people.mozilla.org/~jorendorff/es6-draft.html#sec-generatorresumeabrupt

where the semantics of yield are given.

-- Marc

(Traceur implements all this.)

2015-02-20 8:32 GMT+01:00 Petka Antonov [email protected]:

@mnieper https://github.com/mnieper nothing about that in the es6 spec,
and v8 doesn't implement it so cannot test


Reply to this email directly or view it on GitHub
#27 (comment)
.

@getify
Copy link

getify commented Feb 20, 2015

As a side note, TBH, I've read those several times before, and I'm still not clear exactly where in there:

  1. the try..finally resumes, and then the finally is given a chance to complete
  2. what happens when a yield is encountered inside the finally.

I don't know if BabelJS is fully up to spec on this return(..) stuff or not, but: bablejs generator yield return finally yield

@sebmck
Copy link

sebmck commented Feb 20, 2015

@getify Babel uses regenerator for generators and async functions. Also important to note that this is also the behaviour of Traceur.

@mnieper
Copy link
Author

mnieper commented Feb 20, 2015

If a yield is encountered inside the finally block, the generator resumes
its normal state.

The return value is saved and returned later (as long as no other return
happens inside the finally block).

See also here: https://bugzilla.mozilla.org/show_bug.cgi?id=1115868

2015-02-20 16:48 GMT+01:00 Sebastian McKenzie [email protected]:

@getify https://github.com/getify Babel uses regenerator
https://github.com/facebook/regenerator for generators and async
functions. Also important to note that this is also the behaviour of
Traceur
https://google.github.io/traceur-compiler/demo/repl.html#function%20*foo()%20%7B%0A%20%20try%20%7B%0A%20%20%20%20yield%201%3B%0A%20%20%7D%0A%20%20finally%20%7B%0A%20%20%20%20yield%202%3B%0A%20%20%7D%0A%20%20yield%203%3B%0A%7D%0A%0Avar%20it%20%3D%20foo()%3B%0A%0Aconsole.log(%20it.next()%20)%3B%0Aconsole.log(%20it.return(4)%20)%3B%0Aconsole.log(%20it.next()%20)%3B%0Aconsole.log(%20it.next()%20)%3B%0A
.


Reply to this email directly or view it on GitHub
#27 (comment)
.

@bmeck
Copy link
Member

bmeck commented Feb 20, 2015

It is the correct behavior, even if it is strange. Just like how finally
may never be invoked if the generator yields during a try block but never
gets resumed. Strange things did happen here. Lots of guarantees have been
thrown out w/ yield.

On Fri, Feb 20, 2015 at 9:48 AM, Sebastian McKenzie <
[email protected]> wrote:

@getify https://github.com/getify Babel uses regenerator
https://github.com/facebook/regenerator for generators and async
functions. Also important to note that this is also the behaviour of
Traceur
https://google.github.io/traceur-compiler/demo/repl.html#function%20*foo()%20%7B%0A%20%20try%20%7B%0A%20%20%20%20yield%201%3B%0A%20%20%7D%0A%20%20finally%20%7B%0A%20%20%20%20yield%202%3B%0A%20%20%7D%0A%20%20yield%203%3B%0A%7D%0A%0Avar%20it%20%3D%20foo()%3B%0A%0Aconsole.log(%20it.next()%20)%3B%0Aconsole.log(%20it.return(4)%20)%3B%0Aconsole.log(%20it.next()%20)%3B%0Aconsole.log(%20it.next()%20)%3B%0A
.


Reply to this email directly or view it on GitHub
#27 (comment)
.

@getify
Copy link

getify commented Feb 20, 2015

@mnieper i know that's what it does, I was saying I haven't yet been able to fully explain that by the wording I see in the spec. I've read through it carefully several times, and I still keep getting lost.

@getify
Copy link

getify commented Mar 10, 2015

btw, I just threw together this quick ugly diagram illustrating my point about how a cancelable async function would fit:

cancelable async

The main takeaway is that this doesn't rely on artificially injecting cancelation through shared scope (i.e., using a side-effect to tell step 2 to abort), nor does it rely on promise cancelation (action at a distance).

It instead treats observation and cancelation as separate capabilities (much like early promise/deferreds did, btw!), which only the initiating code (main() in the diagram) receives, leaving the promise to be shared with others in a purely read-only observation role.

@bmeck
Copy link
Member

bmeck commented Apr 7, 2015

other use case that is fairly important that we are using from generator-runner code:

async function () {
  try {
     // init cleanup directives
     let lock = await lockfile(file);
     lock.oncompromised = cancel;
     // stuff that should stop if lock becomes compromised
  }
  finally {
    // cleanup
  }
}

@lukehoban
Copy link
Collaborator

I think this question is really just about what the de facto or de sure standard will be for Promise cancellation.

I personally think a token-based cancellation model that is passed in separately from the Promise instances is a good option for this, that also works great with async/await (see C# precedent for what this looks like at scale).

But I'm not sure there is anything that can/should be changed in the async/await proposal separate from what needs to be done in furthering the models for cancellation around Promise-based APIs.

@bmeck
Copy link
Member

bmeck commented Apr 9, 2015

@lukehoban my concern is when you need to pass the ability to cancel to APIs in use by the async function like the lockfile example I put up.

@getify
Copy link

getify commented Apr 9, 2015

Having to pass around a cancelation token definitely harms the composability of many async functions, unless some de facto standard emerges where the first (or last) argument is always this token, kinda like the ugly "error-first" style of callbacks in node.

Cancelation of promises is way, way far away from a given. A lot of high-visibility folks are already assuming it'll happen, but there are others (like me) who are ready to die on a sword to try to stop that from happening.

This is not as simple as just "wait for promises to figure it out". Promises shouldn't be cancelable. If that means that async functions shouldn't / won't be cancelable, fine. Just say that. It means some of us won't use them, at least not in all cases, but that's a better outcome than just following the cancelation crowd.


Consider this for a moment: let's say that promises do become cancelable. What does that mean for a running (paused) async function instance if such a signal were somehow received? Is it going to end up doing something almost like what generators do, where it aborts the running context, but then still lets any waiting finally's run? If that's true, then why not actually follow generator's lead design wise and go the direction of having an async function return a controller (as I suggested earlier in the thread) of some sort, which includes both the promise and an abort/cancel/whatever trigger?

This spec could take the lead and come up with something that makes more sense than cancelable promises, and then maybe the fetch(..) crowd could follow that. I tried providing feedback into the fetch(..) thread, but my concerns were mostly disregarded. I have a naive hope that maybe this spec/process will have a more sensible outcome.

@bmeck
Copy link
Member

bmeck commented Apr 9, 2015

I would just like to reinforce, it is not always the outside (where a controller would be) that wants to be able to stop execution, sometimes conditions that compromise a task need to be hooked up inside of the function itself. For this use case I am not sure promise cancellation is even a _viable_ solution, though things like generator's .return are how we are doing this right now.

@getify
Copy link

getify commented Apr 9, 2015

@bmeck perhaps I missed it, but if the cancelation signal needs to come from inside the async function, why can't throw (from inside a try..finally) work?

@bmeck
Copy link
Member

bmeck commented Apr 9, 2015

@getify timing is the issue, right now you can throw which will work if you do not have out of band cancellation.

In my example, we need to allow the lock to fire a callback to cancel our function. This is because the lockfile library is the authority on _when_ the lock is compromised. If we want to emulate this with try/catch/finally we would need any await to be followed with a check for any cancellations. This would be more akin to return than throw.

@getify
Copy link

getify commented Apr 9, 2015

out of band cancellation.

Nods. I guess I was assuming all such out-of-band-cancelation could be given access to the controller. which, as you said, already maps to essentially what you're doing now. I can't envision another way of that working.

@getify
Copy link

getify commented May 1, 2015

Just wanted to add another piece of information in support of my suggested "controller" object pattern: Revocable Proxy.

Proxy.revocable(..) returns an object with two properties, the proxy itself and the revoke() function. This is exactly analogous to what I'm suggesting should be the return value from an async function, except instead of revoke() we should call it return() or cancel(), and promise instead of proxy.

Object destructuring makes this still super easy to deal with:

var {promise:p1} = asyncFn();

var {promise:p2,cancel} = asyncFn();
// later... `cancel()`

@bterlson
Copy link
Member

bterlson commented Aug 3, 2015

I'm closing this for now. Once the library space figures out how cancellation should work we can start talking about syntax.

@bterlson bterlson closed this as completed Aug 3, 2015
@felixfbecker
Copy link

Imo promises and async/await is an abstraction to make async code look sync. 99% of times it will be enough - like replacing node errbacks. If the abstraction doesn't fit the use case, than something else than promises (and therefor async/await) should be used, like observables.
Ben Lesh gave a great talk about observables at ng-europe last month and why promises just don't work for ajax requests, for example because they can't be canceled. https://www.youtube.com/watch?v=KOOT7BArVHQ

@RangerMauve
Copy link

I'm totally in agreement that if you want it to be cancellable, that promises aren't the correct primitive to use, and therefore async functions aren't the correct primitive to use.

@getify
Copy link

getify commented Nov 9, 2015

async functions don't have to return promises. that's a choice (and a bad one IMO). this is tail-wagging-the-dog behavior to work backwards from "i want to cancel" to "promises shouldn't be cancelable" to "async functions are the wrong primitive for cancelable actions".

@RangerMauve
Copy link

to obviate most of the need for the generator + promise pattern

Yeah, the pattern in popular libraries such as co which makes no mention of cancellation.

Your personal library, asyquence is cool, however it's not nearly as prevalent.

@getify
Copy link

getify commented Nov 9, 2015

it still seems like what you want is just something that isn't promises.

That's just not true. I consider promises an observation of a future-value completion. That's a read-only operation. I don't think promises should have any external notion of change or cancelation.

But that's entirely orthogonal to the idea that the set of tasks that an async function performs might need to be externally interrupted. Cancelation is an entirely separate capability and concern from wanting to observe if that function finished or not.

@bterlson
Copy link
Member

bterlson commented Nov 9, 2015

@getify What is the standard cancellation mechanism people exposed? It doesn't exist, there are many conventions. And those uses are far outweighed by people who don't care about cancellation and use promises as they are. Async functions as they are now are for this vast majority.

I understand you consider cancellation a critical scenario but I simply disagree with you that cancellation is so important that it needs to be addressed before shipping async functions.

Many people clearly want cancellable promises (it has been added to many libraries and discussed far and wide). Future cost has certainly been discussed (search the meeting notes). I also agree with your concern (as I've stated), I just disagree with your proposed solution (again, as I've stated). I don't know where you read that the goal of async functions is to obviate generator+promise pattern (as I've stated, the goal is to replace promise-returning functions).

@RangerMauve
Copy link

I don't think promises should have any external notion of change or cancelation.

Async functions should have the same semantics of promises since they're trying to be the same as other functions that return promises, just with nice syntax for doing more async actions internally if needed.

@getify
Copy link

getify commented Nov 9, 2015

as I've stated, the goal is to replace promise-returning functions

That makes absolutely no sense to me. Would you say that the design motivation behind generators was to replace iterator-returning functions? That completely discounts all the power/magic of locally-pausable stacks that both of them afford.

The predominant view of the generators + promises pattern is that its primary goal has nothing to do with the return value being an iterator -- that's an implementation detail -- but rather that yield lets you wait on a promise in a synchronous fashion, meaning you don't need nested callbacks or promise chains to express asynchronous series of operations.

The only problem is that this pattern requires a lib to hook up the yielded promise to the iterator.next() call.

Then enters async .. await, which does exactly that for you. I don't see how anyone can argue that async .. await is predominantly about its return value and not about the localized sync-looking semantics inside it.

I'm utterly lost on that assertion.

@getify
Copy link

getify commented Nov 9, 2015

I simply disagree with you that cancellation is so important

cancelation of async operations has been long precedented before any of the current mechanisms we're discussing arrived on the scene. The most common example would be cancelable Ajax, which has been around a long time (even longer in libs).

@RangerMauve
Copy link

I don't see how anyone can argue that async .. await is predominantly about its return value and not about the localized sync-looking semantics inside it.

Again, if we look at the comparisons the community is predominantly using the pattern without cancellation semantics.

@getify
Copy link

getify commented Nov 9, 2015

@RangerMauve --

  1. relative metrics of my lib versus some other lib are not relevant to the conversation. i never said my lib was somehow a sufficient argument for language design. I only mentioned it as a side note.
  2. you missed my point entirely. the statement you quoted has zero to do with cancelation and everything to do with how literally everyone is currently using async .. await and generators+promises... localized pause-resume over promise continuations.

@RangerMauve
Copy link

@getify

  1. Would you like to show me real world statistics about how your proposed semantics are more popular within the community?
  2. Yeah, I got the wrong quote there, sorry! I agree with the point that people are mostly using generators for the pausing ability. However I don't agree with the idea that your particular use, with cancellations, is what everyone is using.

@bmeck
Copy link
Member

bmeck commented Nov 9, 2015

@RangerMauve this brings up a good question though, how many people actually want cancellation, and how many can cancel operations. A lot of existing code does not even have a means for cancellation since all the existing libraries using callbacks etc. could easily place cancellation inside of the control flow code. I would like to avoid using the "everyone is doing it, so it must be better" approach when things such as Compositional Functions are a superset of async...await and can be opted in (heck async could just be a reserved one).

I am on the list of people who actually use cancellation, I have my own generator task library. That being said, most people don't even understand the use case for cancellation since most DOM/npm APIs don't have cancellation abstractions. That said, all of the Promise cancellation abstractions are interesting, but have yet to find one that handles my use cases or those involving resource locks.

@bterlson
Copy link
Member

bterlson commented Nov 9, 2015

Yes async functions make async code look sync. It does not accomplish this with a generator, the generator is just your chosen implementation detail (and conceptual model) of the async function body.

I've already addressed most of your above points and I'm not interested in continuing to restate them.

@getify
Copy link

getify commented Nov 9, 2015

What is the standard cancellation mechanism people exposed?

If you're looking for language precedent (which I'd argue is stronger than user-land precedent), look at the iterator returned by generators. It's a compound object that returns methods for observing (and controlling) the output of the operation (aka next(..)). It also returns another capability: the throw() and return() methods, for interruption/cancelation.

I drew my inspiration for the controller object returned from an async function directly from the design of generators returning iterators. To me, they're isomorphic.

@getify
Copy link

getify commented Nov 9, 2015

Would you like to show me real world statistics about how your proposed semantics are more popular within the community?

My proposed semantics meaning how people use my lib? I didn't argue that. As we're all clearly aware, relatively few people use my lib.

My proposed semantics meaning the design of a controller object coming back from an async function? Zero, since the language hasn't done it.

However, there's a whole lot of people using the iterator controller object coming back from generators, and that's the direct inspiration and (language) precedent for that idea.

@RangerMauve
Copy link

@bmeck

how many people actually want cancellation, and how many can cancel operations

In terms of node, the only things that are particularly cancellable are things with streaming APIs

In the browser it's pretty much the same with XHR and Websockets that are cancellable.

Really, this leads me to re-enstate that Observables or the such are a better primitive for working with cancellable actions.

@bmeck
Copy link
Member

bmeck commented Nov 9, 2015

@RangerMauve I agree, and just want an option to use alternatives with similar syntax to async/await. There is a proposal that allows this already without trying to be as inflexible. While being inflexible is often an advantage to specifications I will continue to talk about it rather than let the idea of flexibility be cast aside.

@RangerMauve
Copy link

which I'd argue is stronger than user-land precedent

New language features should exist to better development for the users, not to satisfy what someone considers to be "a stronger language precedent". The usage of language features by the community should definitely be considered more heavily in development of new features, otherwise who are those features being made for? Just to add bloat?

@getify
Copy link

getify commented Nov 9, 2015

@RangerMauve using language precedent for future design is one way of achieving language design consistency, which leads to better learnability, etc.

Paving the temporary hack cowpaths that userland comes up with while waiting around impatiently on the language actively does not aid in that particular goal.

@getify
Copy link

getify commented Nov 9, 2015

is there anyone seriously saying right now, "that iterator-returned-from-generator design decision was bad, because for example it made chaining multiple sequential generators together harder/more awkward"?

If not, then I think the design of iterator-controller-object-as-return-value is just as valid a suggestion/inspiration for language design as anything else userland comes up with.

@felixfbecker
Copy link

Just because a function isn't labelled async doesn't mean it is not async. That's how it is in javascript. Everywhere you do a setTimeout a normal function goes async. You can do everything in an async function - call callbacks, chain promises, subscribe to events, cancel an observable, ...

But one of the most common usecases is just returning a single value async, throwing async, catching that async exception. Just like we do in synchronous code. And for that simple usecase, async/await provides syntactic sugar. But for everything else, where it goes beyond what sync code would do - everything other than return, try, catch, throw - cancellation, multiple return values, retrying, streaming, progress events - use objects like Observable, Streams, EventEmitter, whatever.
These objects could still be awaited if they implement a then() method and if you're only interested in the return value. Or, you could even first manipulate the object and then await them anyway. But this is not in the scope of async functions.

@sebakerckhof
Copy link

@getify I just stumbled ac cross this discussion because I need to make things cancellable in a project that makes heavy use of async/await. If they had gone with your suggestions, my life would have been tremendously easier now.

@bughit
Copy link

bughit commented Jun 26, 2019

async programming in js is not just one possible path (as it is in other languages) it's the only path, so its async primitives should be designed to be more than demoware.

That async functions can create huge chains of resumable code blocks that can't be externally cancelled by simply not resuming, which is low hanging fruit, is just ridiculous.

Has tc39 ever done a retrospective on this? Any acknowledgement that this was a mistake?

@determin1st

This comment has been minimized.

@vincerubinetti
Copy link

vincerubinetti commented Jan 7, 2020

For anyone who's stumbled on this looking for a solution, I found this to be a great article and work-around:
https://dev.to/chromiumdev/cancellable-async-functions-in-javascript-5gp7

@getify
Copy link

getify commented Jan 7, 2020

@vincerubinetti FWIW, I'm not a fan of the approach in that article, as it doesn't truly proactively cancel the functions immediately, only silently when they later resume. I wrote and now use CAF for managing async functions with true-cancellation semantics.

@vincerubinetti
Copy link

vincerubinetti commented Jan 7, 2020

@getify That looks like a good library, I will definitely look into using it for my project.

I'm not exactly sure what you mean by the statement "it doesn't truly cancel". Do you mean it doesn't cancel in-flight requests like fetches? If so, the article does kinda mention that at the bottom as an after-thought (not super helpful, I know). In my stackoverflow post I give an example of how to integrate AbortController into the method as well. Do you mean that your library can also "in-flight" cancel things besides fetches, like computationally intensive arbitrary asynchronous functions?

It seems to me that there's really nothing wrong with the article's approach, and would still be useful to a lot of people, as long as you understand what it's doing (which can be said of any solution really). If I understand how it works correctly, it just makes you split up your function into break points (in between yields) that you can cancel (essentially instantly) between. You of course can't can't cancel things running in-between the yield markers. But it still allows you to break up things that might take long into smaller pieces so you can exit/cancel in between them and save some time.

But this is just from a very cursory look at your library docs and src, I might be missing something. I'll look into it more later.

*EDIT, also I should clarify that I didn't LIKE the solution, but I did find it to be the best available option (before I knew about your CAF library). The best solution would've been something built into javascript itself, of course.

@getify
Copy link

getify commented Jan 7, 2020

I don't want to get too OT for this thread, but briefly...

not exactly sure what you mean by the statement "it doesn't truly cancel".

I mean that the article's concept of comparing nonces doesn't do the comparison until after the await/yield has resolved, meaning that the ajax request (or whatever) has fully completed. If it stays pending, potentially forever, this async function/generator is just left in that pending state and not cleaned up.

Generators are nice for this use-case precisely because they can be stopped immediately using the return/throw methods.

IOW, even if you didn't signal the cancellation down into the fetch() call, I would consider the async function being "truly cancelled" if the generator was aborted right away and not continuing to wait for that operation to finish or not.

Do you mean that your library can also "in-flight" cancel things besides fetches, like computationally intensive arbitrary asynchronous functions?

CAF uses AbortController cancellation tokens, so they can indeed be passed along to any other utilities that know what to do with AC instances, such as fetch(..), which means that you can get "deep cancellation" of the whole task, not just shallow cancellation.

I'm hopeful that the JS community follows the lead of fetch() (and CAF) and standardizes widely on AC for cancellation.

@determin1st
Copy link

generic promise cancellation is not possible and doesnt make sense.
i told you but you dont want to listen - you want to write texts.

@cgd1
Copy link

cgd1 commented Jul 25, 2020

generic promise cancellation is not possible and doesnt make sense.
i told you but you dont want to listen - you want to write texts.

9 down votes say it does

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

No branches or pull requests