Alternative way to achieve cancelable promise

# Kagami Rosylight (8 years ago)

I want to find a way to replace cancellation token in the current stage 1 cancelable promise proposal with an alternative way which does not require passing an additional parameter.

Here is a short representation of my current thought:

// A cancelable object supports new `Symbol.cancel`.
// `object[Symbol.cancel]()` will cancel any tasks related to the object.
interface Cancelable {
  [@@cancel](): void;
}

// A promise *may* be cancelable
interface Promise extends Cancelable {}
// Here, a new `chain` object from promise constructor callback will
// help chaining cancelable tasks and provide cancellation related
// helper functions.

function foo() {
  return Promise.cancelable(async (chain) => {
    await nonCancelableSubWork1();
    chain.throwIfCanceled(); // This line will throw `Cancel` object if the promise got a cancellation request
    await nonCancelableSubWork2();
 });
}

function bar() {
  return Promise.cancelable(async (chain) => {
     // This `chain()` call will register foo() to the cancellation chain of a promise instance
     // and will propagate cancellation to the chained tasks.
     // The chain can receive any object that supports `Symbol.cancel`.
     await chain(foo());
     await chain(baz());
 });
}

const promise = bar();
promise.cancel(); // This will cancel `bar` call and the cancellation will propagate to `foo` and `baz`

And with some syntax sugar for readability:

cancelable function foo() {
  // `chain` is a keyword inside cancelable function blocks
  await nonCancelableSubWork1();
  chain.throwIfCanceled(); // similar form like `new.target`
  await nonCancelableSubWork2();
}

cancelable function bar() {
  chain foo();
  chain baz();
}

const promise = bar();
promise.cancel();

cancelable function baz() {
  try {
    chain baw();
  }
  else {
    // try-else block will catch cancellation, as the current existing proposal does
  }
}

I think this will achieve easier and more readable flow of cancelable tasks, what do you think?

# Marky Mark (8 years ago)

I must admit that I do like this approach, however I'm not too familiar with the proposed spec to definitively comment on this. Have you tried proposing this in the tc39/proposal-cancelable-promises repository? That may be a better avenue for your suggestions.

# Kagami Rosylight (8 years ago)

Thank you for your interest in my proposal. :D

I also thought so and I posted an older version of my proposal on June there. I got this answer:

This is a separate proposal, and not an issue with the proposal being developed in this repo. So, I'll close this issue. You're welcome to continue using it as a discussion thread for your proposal, but in general there are better places to do that, such as es-discuss or your own GitHub repo you created for your proposal.

Thus I’m posting this here separately to gain attention and discuss.

# Bergi (8 years ago)

Kagami Rosylight wrote:

I want to find a way to replace cancellation token in the current stage 1 cancelable promise proposal with an alternative way which does not require passing an additional parameter.

You're not the only one who is unsatisfied with the current proposal :-) Also have a look at

Here is a short representation of my current thought:

// A cancelable object supports new `Symbol.cancel`.
// `object[Symbol.cancel]()` will cancel any tasks related to the object.
interface Cancelable {
  [@@cancel](): void;
}

interface Promise extends Cancelable {}

That's not going to happen. Promises are result values that can be passed to multiple consumers, and not every consumer should be allowed to cancel the computation. So by default, promises must not be cancellable. There could be such promises that can be cancelled by whomever gets a hold on them - they are known as Tasks iirc - but that needs to be an opt-in.

// Here, a new chain object from promise constructor callback will // help chaining cancelable tasks and provide cancellation related // helper functions.

I don't see the major difference between these "chain" objects and "tokens" from the other proposals. Can you expand on that, please?

function foo() { return new Promise(async (resolve, reject, chain) => { await nonCancelableSubWork1(); chain.throwIfCanceled(); // This line will throw Cancel object if the promise got a cancellation request await nonCancelableSubWork2(); resolve(); }); }

That's not going to work. You should never pass an async function to the new Promise constructor, have a look here. Fortunately, the code in your actual proposal seems more reasonable here.

And with some syntax sugar for readability:

cancelable function foo() {
  // `chain` is a keyword inside cancelable function blocks
  await nonCancelableSubWork1();
  chain.throwIfCanceled(); // similar form like `new.target`
  await nonCancelableSubWork2();
}

cancelable function bar() {
  chain foo();
  chain baz();
}

const promise = bar();
promise.cancel();

If I understood correctly, your chain keyword could be used like await? What is the difference between them?

But I really like the idea of cancelable function sugar that does the housekeeping implicitly and returns cancellable promises automatically. This very much reminds me of my own ideas bergus/promise-cancellation/blob/master/enhancements.md :-)

kind , Bergi

# Kagami Rosylight (8 years ago)

You're not the only one who is unsatisfied with the current proposal :-) Also have a look at

Great! I’m yet to read them but I definitely will soon to discuss more.

Promises are result values that can be passed to multiple consumers, and not every consumer should be allowed to cancel the computation. So by default, promises must not be cancellable.

My current updated proposal allows cancellation only for promises created by Promise.cancelable() call. In this case, simple return Promise.resolve(cancelablePromise) will disallow cancellation for other consumers. But yes, this may be an opt-out rather than opt-in for APIs returning cancelable promises. (PS on 10 April 2017: existing APIs may require { cancelable: true } to return cancelable promises.)

I don't see the major difference between these "chain" objects and "tokens" from the other proposals. Can you expand on that, please?

“Chain”s do not need to be passed manually. A chain will store cancelable promises and will [@@cancel]() them after cancellation request. Each promise will have its own chain which will again propagate cancellations.

You should never pass an async function to the new Promise constructor, have a look here. Fortunately, the code in your actual proposal seems more reasonable here.

I agree that the constructor approach is not good and would break easily. Thus, my updated proposal now uses Promise.cancelable(async (chain) => {}) rather than the constructor itself.

If I understood correctly, your chain keyword could be used like await? What is the difference between them?

await will not be automatically canceled when chain will:

cancelable function saya() {
  // This is theoretically cancelable but user somehow want it to be ‘atomic’.
  await cancelableSubWork();
  chain.throwIfCanceled();
}

Sincerely, Kagami Sascha Rosylight

# Kagami Rosylight (8 years ago)

So by default, promises must not be cancellable.

With some API change now the proposal allows this, similar to other token based proposals:

// if you don’t want to allow other consumers cancel this call
// for security reasons or whatever, you can disallow it by
// creating chain explicitly:
function foo() {
  const chain = new CancelableChain();

  // here myFetch() will register to cancellation chain and will be canceled
  // by chain.cancel()
  return myFetch(“url”, { chain });
}