4

Is there a way to cancel a ES7 async function?

In this example, on click, I want to abort async function call before calling new.

async function draw(){
  for(;;){
    drawRandomRectOnCanvas();
    await sleep(100);
  }
}

function sleep(t){
  return new Promise(cb=>setTimeout(cb,t));
}

let asyncCall;

window.addEventListener('click', function(){
  if(asyncCall)
    asyncCall.abort(); // this dont works
  clearCanvas();
  asyncCall = draw();
});
Felix Kling
  • 795,719
  • 175
  • 1,089
  • 1,143
Yukulélé
  • 15,644
  • 10
  • 70
  • 94
  • yes @spender, of course, thank! (edited) – Yukulélé Oct 01 '15 at 21:52
  • possible duplicate of [How to cancel an EMCAScript6 (vanilla JavaScript) promise chain](http://stackoverflow.com/q/29478751/1048572) and [a few others](http://stackoverflow.com/questions/tagged/cancellation+promise) – Bergi Oct 01 '15 at 21:57

2 Answers2

6

There's nothing built in to JavaScript yet, but you could easily roll your own.

MS.Net uses the concept of a cancellation token for the cancelling of Tasks (the .net equivalent of Promises). It works quite nicely, so here's a cut-down version for JavaScript.

Say you made a class that is designed to represent cancellation:

function CancellationToken(parentToken){
  if(!(this instanceof CancellationToken)){
    return new CancellationToken(parentToken)
  }
  this.isCancellationRequested = false;
  var cancellationPromise = new Promise(resolve => {
    this.cancel = e => {
      this.isCancellationReqested = true;
      if(e){
        resolve(e);
      }
      else
      {
        var err = new Error("cancelled");
        err.cancelled = true;
        resolve(err);
      }
    };
  });
  this.register = (callback) => {
    cancellationPromise.then(callback);
  }
  this.createDependentToken = () => new CancellationToken(this);
  if(parentToken && parentToken instanceof CancellationToken){
    parentToken.register(this.cancel);
  }
}

then you updated your sleep function to be aware of this token:

function delayAsync(timeMs, cancellationToken){
  return new Promise((resolve, reject) => {
    setTimeout(resolve, timeMs);
    if(cancellationToken)
    {
      cancellationToken.register(reject);
    }
  });
}

Now you can use the token to cancel the async function that it was passed to:

var ct = new CancellationToken();
delayAsync(1000)
    .then(ct.cancel);
delayAsync(2000, ct)
    .then(() => console.log("ok"))
    .catch(e => console.log(e.cancelled ? "cancelled" : "some other err"));

http://codepen.io/spender/pen/vNxEBZ

...or do more or less the same thing using async/await style instead:

async function Go(cancellationToken)
{
  try{
    await delayAsync(2000, cancellationToken)
    console.log("ok")
  }catch(e){
    console.log(e.cancelled ? "cancelled" : "some other err")
  }
}
var ct = new CancellationToken();
delayAsync(1000).then(ct.cancel);
Go(ct)
vkurchatkin
  • 13,364
  • 2
  • 47
  • 55
spender
  • 117,338
  • 33
  • 229
  • 351
  • Why do I need `self`? –  Oct 02 '15 at 04:46
  • I'm not sure I would feel safe with a pub/sub pattern. It's open to abuse by me or members of my team. A token could have multiple registrations and any particular registration could do absolutely anything, not necessarily a promise cancellation. Unless I'm in full charge the token's deployment and the code to which it is deployed, I have no idea of the consequences of calling `ct.cancel()`. I'd be a lot more comfortable with a solution that gave me promises each with its own `.cancel()` method. That way, total carnage could still happen but not quite so readily. – Roamer-1888 Oct 02 '15 at 06:37
  • @torazaburo Mainly out of comfort and because I'm a bit wooly on how arrow functions bind. – spender Oct 02 '15 at 08:28
  • @Roamer-1888 On the other side of the coin, this is also where a cancellation token shines. You can pass a single token into an async function that might, in turn call several other async functions. You don't need to keep a register of your "pending" promises in order to decide what to cancel, you just cancel the token once, and the whole chain is shut down... – spender Oct 02 '15 at 08:37
  • @spender, yes indeed. Your solution is one to keep in my back pocket for some future occasion. – Roamer-1888 Oct 02 '15 at 09:58
  • @torazaburo After re-reading about how arrow functions bind to `this`, I've removed `self` from the mix. – spender Oct 02 '15 at 14:44
  • you can't use this with async/await – vkurchatkin Oct 02 '15 at 22:47
  • @vkurchatkin I don't know how you tested this, but you most definitely can use this with async/await. See the additional code I added to demonstrate this. Can I get that downvote back? – spender Oct 03 '15 at 02:27
  • fine, that would work, but it's not really usable without automatic propagation – vkurchatkin Oct 03 '15 at 06:38
  • @vkurchatkin : You mean propagation "up" the Promise chain? Why do you think this this is needed? – spender Oct 03 '15 at 10:48
  • @spender, thinking about this some more, with one token shutting down a whole chain, have you considered the interaction between the token's action and any intermediate catches? – Roamer-1888 Oct 03 '15 at 11:12
  • I mean that you need to pass token explicitly to each asynchronous function – vkurchatkin Oct 03 '15 at 11:54
3

Unless your question is purely theoretical, I assume you are using Babel, Typescript or some other transpiler for es6-7 support and probably some polyfill for promises in legacy environments. Though it's hard to say what will become standard in the future, there is a non-standard way to get what you want today:

  1. Use Typescript to get es6 features and async/await.
  2. Use Bluebird for promises in all environments to get sound promise cancellation support.
  3. Use cancelable-awaiter which makes Bluebird cancellations play nice with async/await in Typescript.
Itay
  • 144
  • 2
  • 8