You can't cancel a JavaScript promise (except sometimes you can) (inngest.com)

by goodoldneon 61 comments 97 points
Read article View on HN

61 comments

[−] pjc50 38d ago
I like how C# handles this. You're not forced to support cancellation, but it's strongly encouraged. The APIs all take a CancellationToken, which is driven by a CancellationTokenSource from the ultimate caller. This can then either be manually checked, or when you call a library API it will notice and throw an OperationCancelledException.

Edit: note that there is a "wrong" way to do this as well. The Java thread library provides a stop() function. But since that's exogenous, it doesn't necessarily get cleaned up properly. We had to have an effort to purge it from our codebase after discovering that stopping a thread while GRPC was in progress broke all future GRPC calls from all threads, presumably due to some shared data structure being left inconsistent. "Cooperative" (as opposed to preemptive) cancel is much cleaner.

[−] teraflop 38d ago
I am surprised that you had to go out of your way to remove Thread.stop from existing Java code. It's been deprecated since 1998, and the javadoc page explains pretty clearly why it's inherently unsafe.

It's hard to miss all the warnings unless you're literally just looking at the method name and nothing else.

[−] pjc50 38d ago
I was certainly surprised to see it when I found it.
[−] kelnos 38d ago
Not to mention that I feel like it's pretty unusual to be creating and managing threads yourself in Java these days, instead of using a thread pool/executor.
[−] wiseowise 38d ago
That’s barely-junior interview question indeed.
[−] AtlasBarfed 38d ago
One of Java's the ecosystem fundamental platforms is that it's multi-threading. It's gone through too many models.

And since Java has a metric ton of blog posts from the 2000s and 2010s, a lot of search engines lead you to older models.java itself has gone from green threads to OS threads and back to green threads now.

[−] DonHopkins 37d ago
Write one, stop anywhere.
[−] esprehn 38d ago
AbortSignal is same thing on the Web. It's unfortunate TC39 failed to ever bring a CancelToken to the language to standardize the pattern outside browsers.
[−] bakkoting 38d ago
Browsers have said that they are unwilling to ship any new cancelation mechanisms given that AbortSignal already exists, so we can't ship a different CancelToken. But I think there's a path to standardizing a subset of the existing AbortSignal machinery [1].

(I am on TC39 and while this isn't my highest priority I did bring the topic for discussion at the last meeting [2], and there was support from the rest of committee.)

[1] https://github.com/tc39/proposal-concurrency-control/issues/...

[2] https://github.com/bakkot/structured-concurrency-for-js

[−] esprehn 38d ago
Yes, I was around during the original discussions. AbortSignal exists because TC39 was taking too long and cancelling fetch() is table stakes for a networking oriented platform like the web.

Those threads are a good example of what's wrong in TC39. A simple AbortSignal could have been added, but by the time it's reconciled with SES, large speculative APIs like Governers, or the original attempt to add a parallel throw mechanism just for cancellation, nothing actually gets done.

It's been 10 years since CancelToken was first discussed and we're still debating it.

[−] bakkoting 37d ago
I agree that the original cancellation discussion was bad. I don't agree that these threads reflect the same disfunction. They're a new effort (from me). No one was working on it previously because browsers have said that they were unwilling to add any other form of cancellation given that AbortSignal already exists, so there was never a chance to add a separate CancelToken once it shipped. The work to be done now is basically administrative: moving a subset from the WHATWG spec to TC39. This has ~no relevance to user's lives unless they're using a JS runtime which does not implement the WinterTC spec, which is approximately no one. The delay has nothing to do with SES (which has no bearing on this), and Governors are a _use case_ which motivates bringing it into the language, not a thing with which it needs to be reconciled.
[−] runarberg 38d ago
TC39 seems to be failing at many things for the past 10 years.
[−] notnullorvoid 38d ago
Hard disagree, TC39 has done great work over the last 10 years. To name a few: - Async/await - Rest/spread - Async iterators - WeakRefs - Explicit Resource Management - Temporal

It's decisions are much more well thought out than WHATWG standards. AbortSignal extending from EventTarget was a terrible call.

[−] runarberg 38d ago
many things !== all the things

More good works from the last 10 years includes .at(), nullish chaining, BigInt etc.

But most of what you mentioned is closing in on 10 years in the standard (Async/Await is from 2017) meaning the bulk of the work done is from over 10 years ago.

The failure of AbortSignal is exactly the kind of failure TC39 has been doing in bulk lately. I have been following the proposal to add Observables to the language, which is a stage 1 proposal (and has been for over 10 years!!!). There were talks 5 years ago (!) to align the API with AbortSignal[1] which I think really exemplifies the inability for TC39 to reach a workable decision (at least as it operates now).

Another example I like to bring up are the failure of the pipeline operator[2], which was advanced to stage-2 four years ago and has been in hiatus ever since with very little work to show for it. After years of deliberation very controversal version of the operator with a massive community backlash. Before they advanced it it was one of the more popular proposals, now, not so much, and personally I sense any enthusiasm for this feature has pretty much vanished. In other words I think they took half a decade to make the obviously wrong decision, and have since given up.

From the failure of the pipeline operator followed a bunch of half-measures such as array grouping, and iterator helpers etc. which could have easily been implemented in userland libraries if the more functional version of the pipeline operator would have advanced.

1: https://github.com/tc39/proposal-observable/issues/209

2: https://github.com/tc39/proposal-pipeline-operator

[−] watermelon0 37d ago
Unless I'm missing something, AbortSignal is quite standardized on backend as well.

Of course not all libraries support it, but many do, and support seems to be growing.

[−] CharlieDigital 38d ago
C# has very good support for this.

You can even link cancellation tokens together and have different cancellation "roots".

[−] torginus 38d ago
I don't like it - you're forced to pass around this token, constantly manage the lifecycle of cancellation sources - and incredibly bug prone thing in async context, and it quickly gets very confusing when you have multiple tokens/sources.

I understand why they did it - a promise essentially is just some code, and a callback that will be triggered by someone at some point in time - you obviously get no quality of service promises on what happens if you cancel a promise, unless you as a dev take care to offer some.

It's also obvious that some operations are not necessarily designed to be cancellable - imagine a 'delete user' request - you cancelled it, now do you still have a user? Maybe, maybe you have some cruft lying around.

But still, other than the obvious wrong solution - C# had a Thread.Abort() similar to the stop() function that you mentioned, that was basically excommunicated from .NET more then a decade ago, I'm still not happy with the right one.

[−] estimator7292 38d ago
Cancelling a token doesn't immediately abort the underlying Task. It is up to the implementation of that task to poll the token and actively decide when to abort.

In your example, you'd design your delete task such that if you want it to be cancelable, it can only be canceled before data is modified. You simply don't abort in the middle of a database transaction.

Moreover, because of the way cancellation tokens work, you can't abort blocking function calls unless you also pass the token along. There just isn't a mechanism that can interrupt a long IO operation or whatever unless you explicitly go to the effort to make that happen.

A cancellation token is more of a "pretty please stop what you're doing when you feel like it" concept than Thread.Abort().

[−] CharlieDigital 38d ago

    > ...constantly manage the lifecycle of cancellation sources
Very rare unless you are spawning your own.

Usually, you are passing through a runtime provided token (e.g. ASP.NET).

[−] zbentley 37d ago
I was always surprised that Python, of all languages, didn’t support a robust Thread.stop.

Before the removal of the GIL in recent years, Python seemed well-positioned to leverage the GIL to offer safe thread-cancellation points that didn’t leave interpreter internals in a corrupted state.

That’s not necessarily an endorsement of the idea of Thread.stop in many cases, since stopped user code can cause broken assumptions at a high level no matter what, but it has its uses. Erlang’s exit/2 is proof of that, though it is a very sharp and rarely-appropriate tool.

[−] sandreas 36d ago
I much prefer Coroutines in Kotlin... Lightweight, pauseable, resumable, cancelable and easy to use without having to provide a CancellationToken AND a CancellationTokenProvider.

I really hope that coroutines land in Rust after being experimental to improve the async stuff.

The C# Tasks are not bad but I think the API could be easier and more precise/clear.

[−] wiseowise 38d ago

> "Cooperative" (as opposed to preemptive) cancel is much cleaner.

Which what Thread.interrupt does.

[−] mohsen1 38d ago
Back in 2012 I was working on a Windows 8 app. Promises were really only useful on the Windows ecosystem since browser support was close to non existent. I googled "how to cancel a promise" and the first results were Christian blogs about how you can't cancel a promise to god etc. Things haven't changes so much since, still impossible to cancel a promise (I know AbortSignal exists!)
[−] eithed 38d ago

> Promise itself has no first-class protocol for cancellation, but you may be able to directly cancel the underlying asynchronous operation, typically using AbortController.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...

[−] shadowgovt 38d ago
I soft of feel like every five years someone comes along and tries to re-invent cancellable threads and immediately arrives back at the same conclusion: the problem of what it means to "cancel" a thread is so domain-specific that you never save anything trying to "support" it in your threading framework; you try and save people the effort of doing something ad-hoc to simulate cancellation and build something at least as complicated as what they would build ad-hoc, because thread cancellation is too intimately tied to the problem domain the threads are operating on to generalize it.
[−] thomasnowhere 38d ago
The never-resolving promise trick is clever but what caught me off guard is how clean the GC behavior is. Always assumed hanging promises would leak in long-lived apps but apparently not as long as you drop the references.
[−] cush 38d ago
GC can be very slow. Relying on it for control flow is a bold move
[−] BlueGreenMagick 38d ago
I don't think the control flow relies on GC.

The control flow stops because statements after await new Promise(() => {}); will never run.

GC is only relied upon to not create a memory leak, but you could argue it's the same for all other objects.

[−] abraxas 38d ago
and so the thirty year old hackathon continues...
[−] littlestymaar 38d ago
Ten years ago, I was an acid reader of the tc39 (EcmaScript standard committee) mailing list and cancelable promises used to be the hot topic for a while.

I unsubscribed at some point because I wasn't working with JavaScript this much, but it's disappointing to see that this work has gone nowhere in the meantime.

I like how Rust futures are canceled by dropping them, even though it's also a footgun (though IMHO this is more of a problem with the select! pattern than with drop-to-cancel proper).

[−] cowboyd 38d ago
Is it safe to just "stop calling next() on a generator?" like the post suggest?

To me that sounds like dropping the task on the floor. Specifically, this will not invoke any finally {} blocks:

More correctly, you should invoke return() on the generator. Otherwise, you won't provide execution guarantees. This is how Effection does it. There is no equivalent in async functions, so it sounds like the same problem would apply to the GC technique.

[−] bastawhiz 38d ago
Be careful with this, though. If a promise is expected to resolve and it never does, and the promise needs to resolve or reject to clean up a global reference (like an event listener or interval), you'll create a memory leak. It's easy to end up with a leak that's almost impossible to track down, because there isn't something obvious you can grep for.
[−] flossypurse 37d ago
I work at Resonate and where this point is somewhat moot if you handle suspension as a first class primitive in the runtime: https://www.smokesomepaint.com/p/is-suspension-a-primitive-o...
[−] notnullorvoid 38d ago
The argument against rejecting to cancel seems like a stretch to me. It's completely fine if you view cancellation as a error condition, it allows you to recover from a cancellation if you want (swallow the error w catch) or to propagate it.
[−] stevefan1999 38d ago
I would also argue that Rust failed to cancel a Future too, considering I came from a C++ and C# background where I know a lot more about async/await and the missing of "cancellation token" in Tokio is so infuriating.

I have to came all the way to attach it to a Future, that because Rust doesn't have any default argument, I mean I have to supply cancellation token from the top to bottom.

But in hindsight, Golang's context actually do mimick cancellation token in some sort, but in a way that the cancellation is cascaded and recursive by using done in canceler or deadline which means it is timing and latency sensitive.

If you truly want cancellation token style, you need to use semaphores or by explicitly polling the cancellation channel (which is what you can do with context) which can hurt if you don't have enough thread slack.

[−] game_the0ry 38d ago
Off topic, but that site has really nice design
[−] dimitropoulos 38d ago

> Libraries like Effect have increased the popularity of generators, but it's still an unusual syntax for the vast majority of JavaScript developers.

I'm getting so tired of hearing this. I loved the article and it's interesting stuff, but how many more decades until people accept generators as a primitive??

used to hear the same thing about trailing commas, destructuring, classes (instead of iife), and so many more. yet. generators still haven't crossed over the magic barrier for some reason.

[−] afarah1 38d ago
You can also race it with another promise, which e.g. resolves on timeout.
[−] ryguz 38d ago
[dead]
[−] TZubiri 38d ago
[flagged]