1 of 26

cancelable promises

Investigations and a proposed direction

2 of 26

Problem statement

  • Many async operations should be “cancelable” (whatever that means)
  • Examples:
    • fetch() and everything that fetches (loading various resources)
    • Web animations
    • Piping together streams
    • Long-running off-main-thread operations (e.g. crypto)
    • Asking the user for a permission
    • Async iteration (or observation)
  • Fitting this into the promise paradigm is tricky
    • No longer an exact analogy with synchronous functions
    • Promises are multi-consumer; do consumers get to cancel? Which ones?
    • How does this impact existing promise-returning APIs?

3 of 26

Discussion roadmap

What is cancelation?

A third promise state

We’ll discuss how cancelation should be represented, independent of how it should be generated

How to cancel

Two approaches

Cancelable promises (“tasks”) vs. cancel tokens.

Next steps

Stage 1?

Is the committee on board with our conclusions? Can we proceed?

4 of 26

Cancelation as a third state

5 of 26

What does cancelation mean?

Is a canceled operation “successful”? Did it “fail”?

  • My answer: neither
  • Canceled is a third state, alongside fulfilled and rejected.
  • It behaves somewhat similar to an exception in terms of propagation, but it does not represent failure.
  • Thus, cancelation going unhandled is expected, and not an error state.
  • https://github.com/domenic/cancelable-promise/blob/master/Third%20State.md

6 of 26

Canceled as a special case of rejection: bad

try {� await fetch(...);�} catch (e) {� if (!(e instanceof CancelError)) {� showUserMessage("The site is down.");� }�} finally {� stopLoadingSpinner();�}

7 of 26

Canceled as a third state: good

try {� await fetch(...);�} catch (e) {� showUserMessage("The site is down.");�} finally {� stopLoadingSpinner();�}

8 of 26

Making it a first-class third state

  • Promise additions:
    • new Promise((resolve, reject, cancel) => { … })
    • Promise.cancel(reason)
    • promise.then(onFulfilled, onRejected, onCanceled)
    • promise.cancelCatch(reason => { … })
  • Language additions:
    • try { … } cancel catch (reason) { … }
    • cancel throw reason
    • generator.cancel(reason)

9 of 26

Third state in action

const canceledPromise = Promise.resolve().then(() => {� throw cancel "the user clicked 'stop'";�});��const rejectedPromise = Promise.cancel().cancelCatch(() => {� throw new Error("bad things happened");�});��const fulfilledPromise = Promise.cancel().cancelCatch(() => {� return 5;�});��// Cancelation propagates unless explicitly reacted to:�const canceledPromise2 = Promise.cancel()� .then(v => { ... })� .catch(e => { ... })� .finally(() => { ... });

10 of 26

Summary

Cancelation as a third state

But, this isn’t useful by itself. How does something become canceled?

11 of 26

Canceling async operations

12 of 26

The story so far

  • If we somehow got a cancelable promise, the third-state framework gives us a great way of reacting to it.
  • Producers could explicitly give us a canceled promise:
    • new Promise((resolve, reject, cancel) => { … })
    • const p = producerAPI.doCoolThing();
    • Now something cancels the doCoolThing operation:
      • Maybe it happens because of external factors
      • Maybe we do producerAPI.stopDoingCoolThing();
    • p is now canceled
  • But this encourages ad-hoc cancelation APIs. We should standardize.

13 of 26

I had a dream: cancelable promise objects (“tasks”)

startSpinner();��const p = fetch(url)� .then(r => r.json())� .then(data => fetch(data.otherUrl))� .then(r => r.text())� .then(text => updateUI(text))� .catch(err => showUIError())� .finally(stopSpinner);��cancelButton.onclick = () => p.cancel();

14 of 26

Problems with tasks

  • Async function integration
  • Upward-propagation of cancel signal at odds with downward-state propagation�https://github.com/domenic/cancelable-promise/issues/8
  • Interoperation with “normal” promises:
    • Can we make all promises tasks? No; people will be unhappy they’re now mutable.
    • Now we’ve split the universe: I can’t pass tasks to promise-handling libraries without losing cancelation capabilities.

15 of 26

The well-known alternative: cancel tokens

  • Pioneered in .NET
  • Adapted for JavaScript by Kevin: https://github.com/zenparsing/es-cancel-token
  • We should adapt it to the “third state” insights, but otherwise good to go

16 of 26

Cancel tokens in action

async function f(cancelToken) {� await a();�� // Only execute `b` if task has not been cancelled.� if (!cancelToken.requested)� await b();�}��const ct = new CancelToken(cancel => {� cancelButton.onclick = cancel;�});��f(ct); // NOTE: will be fulfilled if they click the cancel button

17 of 26

The cancel token API

new CancelToken(cancel => { … }) // revealing constructor��cancelToken.requested // boolean��cancelToken.promise // fulfills when requested��cancelToken.cancelIfRequested(reason);�// `throw cancel reason` if requested

18 of 26

Cancel tokens in action

async function f(cancelToken) {� await a();� cancelToken.cancelIfRequested();� await b();�}��const ct = new CancelToken(cancel => {� cancelButton.onclick = cancel;�});��f(ct); // NOTE: will be canceled if they click the cancel button

19 of 26

Advantages of cancel tokens

  • Conceptually separate “return value” (promise) from “operation” (function) and “cancelation capability” (token that operation cooperates with)
  • Can pass the same cancel token around everywhere
  • Can be reused in many different contexts, not just promises
  • Unambiguous, simple semantics
    • Producer completely controls their reaction to the cancel token
    • State propagation is still always “downward”

20 of 26

Disadvantages of cancel tokens

  • Simple cancelation is awkward�https://github.com/zenparsing/es-cancel-token/issues/3
  • Parameter list pollution�https://github.com/zenparsing/es-cancel-token/issues/2
    • Does not fit well with JS’s lack of static overload resolution
    • Makes retrofitting onto existing APIs difficult
  • Sometimes-strange naming choices to avoid “canceled” vs. “cancelled” :)

21 of 26

Summary

How to cancel

Cancel tokens are our best bet. They have some good advantages, and livable disadvantages.

22 of 26

The full proposal

23 of 26

A two-part proposal

  • Cancelation as a third state
  • Cancel tokens
    • Build on the notion of throw cancel from cancelation as a third state
    • Otherwise independent
    • https://github.com/zenparsing/es-cancel-token

24 of 26

Stage 1?

  • Champion: myself
  • Both parts of the proposal are fleshed out in reasonable detail:
    • Cancelation as a third state has a rationale document, a reference implementation, and tests
    • Cancel tokens have an explainer and API surface document
  • Lots of illustrative examples exist
  • The high-level API is decided, although bikeshedding is welcome
  • Key algorithms, abstraction, and semantics are good to go
  • This deck is all about the cross-cutting concerns and challenges

25 of 26

We’d like to move fast

  • Multiple web platform features have been waiting for cancelable promises
  • My bad for taking so long to come up with a proposal
  • Hopeful that by going with cancel tokens, despite their disadvantages, many of the contentious issues vanish
  • Spec text and stage 2 next time?
  • Reviews and stage 3 after that?

26 of 26

Thank you