81

All four functions are called below in update return promises.

async function update() {
   var urls = await getCdnUrls();
   var metadata = await fetchMetaData(urls);
   var content = await fetchContent(metadata);
   await render(content);
   return;
}

What if we want to abort the sequence from outside, at any given time?

For example, while fetchMetaData is being executed, we realize we no longer need to render the component and we want to cancel the remaining operations (fetchContent and render). Is there a way to abort/cancel these operations from outside the update function?

We could check against a condition after each await, but that seems like an inelegant solution, and even then we will have to wait for the current operation to finish.

Wenfang Du
  • 8,804
  • 9
  • 59
  • 90
sbr
  • 4,735
  • 5
  • 43
  • 49
  • 3
    The same we you do it in "normal" code: Either return a value indicating that something is not available (e.g. `null`) or throw an exception. In both cases the caller has to decide how to handle them. – Felix Kling Jun 03 '16 at 22:09
  • 1
    i want the caller to explicitly abort the flow. how can that be done – sbr Jun 03 '16 at 22:38
  • 2
    Let `fetchMetaData` return `null` and do `if (!metadata) { return; }`/ – Felix Kling Jun 03 '16 at 22:40
  • Alternatively, let your functions do this - as if your values were in the `Option` monad – Bergi Jun 03 '16 at 22:50
  • 2
    the point is var updateComponent = update(); // after a few seconds, the user clicks away and new we have to tear down this rendering . how can we then short circuit the flow inside update. Note the difference between explicit action to abort vs inside action to return from the function – sbr Jun 03 '16 at 22:53
  • So you are are saying the invalidity of the component is an external event. It sounded like `fetchMetaData` would determine that.... In that case there is not much you can do. Promises are not cancelable. You could rely on some shared global state, or use something different than promises. – Felix Kling Jun 04 '16 at 02:02
  • "*Is there a way to abort/cancel from outside by consumer?*" - [No](http://stackoverflow.com/q/29478751/1048572) – Bergi Jun 04 '16 at 12:58
  • 1
    I suggest you look into this project: https://github.com/Mitranim/posterus – Christophe Marois Aug 16 '17 at 18:33
  • 1
    Two other resources that might be useful to people stumbling on this question: 1) https://dev.to/chromiumdev/cancellable-async-functions-in-javascript-5gp7 A method of turning an `async` function into a generator and running it step by step with cancellation in between steps (perhaps a half-measure). 2) https://github.com/getify/CAF Perhaps a more full-measure, a library that wraps your generator and can "truly" cancel it at any time. – V. Rubinetti Jan 08 '20 at 16:42

10 Answers10

61

The standard way to do this now is through AbortSignals

async function update({ signal } = {}) {
   // pass these to methods to cancel them internally in turn
   // this is implemented throughout Node.js and most of the web platform
   try {
     var urls = await getCdnUrls({ signal });
     var metadata = await fetchMetaData(urls);
     var content = await fetchContent(metadata);
     await render(content);
   } catch (e) {
      if(e.name !== 'AbortError') throw e;
   }
   return;
}
// usage
const ac = new AbortController();
update({ signal: ac.signal });
ac.abort(); // cancel the update

OLD 2016 content below, beware dragons

I just gave a talk about this - this is a lovely topic but sadly you're not really going to like the solutions I'm going to propose as they're gateway-solutions.

What the spec does for you

Getting cancellation "just right" is actually very hard. People have been working on just that for a while and it was decided not to block async functions on it.

There are two proposals attempting to solve this in ECMAScript core:

  • Cancellation tokens - which adds cancellation tokens that aim to solve this issue.
  • Cancelable promise - which adds catch cancel (e) { syntax and throw.cancel syntax which aims to address this issue.

Both proposals changed substantially over the last week so I wouldn't count on either to arrive in the next year or so. The proposals are somewhat complimentary and are not at odds.

What you can do to solve this from your side

Cancellation tokens are easy to implement. Sadly the sort of cancellation you'd really want (aka "third state cancellation where cancellation is not an exception) is impossible with async functions at the moment since you don't control how they're run. You can do two things:

  • Use coroutines instead - bluebird ships with sound cancellation using generators and promises which you can use.
  • Implement tokens with abortive semantics - this is actually pretty easy so let's do it here

CancellationTokens

Well, a token signals cancellation:

class Token {
   constructor(fn) {
      this.isCancellationRequested = false; 
      this.onCancelled = []; // actions to execute when cancelled
      this.onCancelled.push(() => this.isCancellationRequested = true);
      // expose a promise to the outside
      this.promise = new Promise(resolve => this.onCancelled.push(resolve));
      // let the user add handlers
      fn(f => this.onCancelled.push(f));
   }
   cancel() { this.onCancelled.forEach(x => x); }
}

This would let you do something like:

async function update(token) {
   if(token.isCancellationRequested) return;
   var urls = await getCdnUrls();
   if(token.isCancellationRequested) return;
   var metadata = await fetchMetaData(urls);
   if(token.isCancellationRequested) return;
   var content = await fetchContent(metadata);
   if(token.isCancellationRequested) return;
   await render(content);
   return;
}

var token = new Token(); // don't ned any special handling here
update(token);
// ...
if(updateNotNeeded) token.cancel(); // will abort asynchronous actions

Which is a really ugly way that would work, optimally you'd want async functions to be aware of this but they're not (yet).

Optimally, all your interim functions would be aware and would throw on cancellation (again, only because we can't have third-state) which would look like:

async function update(token) {
   var urls = await getCdnUrls(token);
   var metadata = await fetchMetaData(urls, token);
   var content = await fetchContent(metadata, token);
   await render(content, token);
   return;
}

Since each of our functions are cancellation aware, they can perform actual logical cancellation - getCdnUrls can abort the request and throw, fetchMetaData can abort the underlying request and throw and so on.

Here is how one might write getCdnUrl (note the singular) using the XMLHttpRequest API in browsers:

function getCdnUrl(url, token) {
    var xhr = new XMLHttpRequest();
    xhr.open("GET", url);
    var p = new Promise((resolve, reject) => {
      xhr.onload = () => resolve(xhr);
      xhr.onerror = e => reject(new Error(e));
      token.promise.then(x => { 
        try { xhr.abort(); } catch(e) {}; // ignore abort errors
        reject(new Error("cancelled"));
      });
   });
   xhr.send();
   return p;
}

This is as close as we can get with async functions without coroutines. It's not very pretty but it's certainly usable.

Note that you'd want to avoid cancellations being treated as exceptions. This means that if your functions throw on cancellation you need to filter those errors on the global error handlers process.on("unhandledRejection", e => ... and such.

Benjamin Gruenbaum
  • 270,886
  • 87
  • 504
  • 504
  • Where did you give that talk? Is it available somewhere? – Bergi Jun 05 '16 at 12:56
  • @Bergi at a local meetup, it was small (~250 people) and in Hebrew, it was 10 minutes short and was just a survey of the recent developments + my personal opinion on promise cancellation. https://docs.google.com/presentation/d/1V4vmC54gJkwAss1nfEt9ywc-QOVOfleRxD5qtpMpc8U/edit was presented and discussed (some discussion in https://github.com/domenic/cancelable-promise/issues/10 , repo not yet updated). Current consensus - tokens for cancellation, no direct promise cancellation without tokens, observables use tokens too. Quite a mess in https://github.com/zenparsing/es-observable/pull/97 . – Benjamin Gruenbaum Jun 05 '16 at 13:09
  • 1
    Ah, nothing big :-) I guess I'm going to put some time into contributing to cancelable-promise… – Bergi Jun 05 '16 at 14:03
  • thanks @BenjaminGruenbaum for the detailed explanation. I am also considering using co.js (github.com/tj/co) instead of async await directly, which implements it using generators and promises and could take a decision based on token state before every call to the .next() – sbr Jun 06 '16 at 16:12
  • @sbr in that case just use bluebird - it'll be faster, perform better, give better error messages and have built in cancellation semantics with its coroutines. – Benjamin Gruenbaum Jun 06 '16 at 21:26
6

You can get what you want using Typescript + Bluebird + cancelable-awaiter.

Now that all evidence point to cancellation tokens not making it to ECMAScript, I think the best solution for cancellations is the bluebird implementation mentioned by @BenjaminGruenbaum, however, I find the usage of co-routines and generators a bit clumsy and uneasy on the eyes.

Since I'm using Typescript, which now support async/await syntax for es5 and es3 targets, I've created a simple module which replaces the default __awaiter helper with one that supports bluebird cancellations: https://www.npmjs.com/package/cancelable-awaiter

Community
  • 1
  • 1
Itay
  • 144
  • 2
  • 8
5

Unfortunately, there is no support of cancellable promises so far. There are some custom implementations e.g.

Extends/wraps a promise to be cancellable and resolvable


function promisify(promise) {
  let _resolve, _reject

  let wrap = new Promise(async (resolve, reject) => {
    _resolve = resolve
    _reject = reject
    let result = await promise
    resolve(result)
  })

  wrap.resolve = _resolve
  wrap.reject = _reject
    
  return wrap
}

Usage: Cancel promise and stop further execution immediately after it

async function test() {
  // Create promise that should be resolved in 3 seconds
  let promise = new Promise(resolve => setTimeout(() => resolve('our resolved value'), 3000))
  
  // extend our promise to be cancellable
  let cancellablePromise = promisify(promise)
  
  // Cancel promise in 2 seconds.
  // if you comment this line out, then promise will be resolved.
  setTimeout(() => cancellablePromise.reject('error code'), 2000)

  // wait promise to be resolved
  let result = await cancellablePromise
  
  // this line will never be executed!
  console.log(result)
}

In this approach, a promise itself is executed till the end, but the caller code that awaits promise result can be 'cancelled'.

Maksim Shamihulau
  • 1,219
  • 1
  • 15
  • 17
  • 1
    Actually, this is exactly what I was looking for. In one of my implementations for scheduler, I have a lot of async operations that reads from DB, fetch from servers, etc. If an async operation times out and does not return...simply call the resolve/reject yourself using a reference, and use a timeout of however much time you see fit. This will also solve memory issues because we ensure the resolve/reject will be executed. – rtsao Mar 08 '21 at 09:06
4

Unfortunately, no, you can't control execution flow of default async/await behaviour – it does not mean that the problem itself is impossible, it means that you need to do change your approach a bit.

First of all, your proposal about wrapping every async line in a check is a working solution, and if you have just couple places with such functionality, there is nothing wrong with it.

If you want to use this pattern pretty often, the best solution, probably, is to switch to generators: while not so widespread, they allow you to define each step's behaviour, and adding cancel is the easiest. Generators are pretty powerful, but, as I've mentioned, they require a runner function and not so straightforward as async/await.

Another approach is to create cancellable tokens pattern – you create an object, which will be filled a function which wants to implement this functionality:

async function updateUser(token) {
  let cancelled = false;

  // we don't reject, since we don't have access to
  // the returned promise
  // so we just don't call other functions, and reject
  // in the end
  token.cancel = () => {
    cancelled = true;
  };

  const data = await wrapWithCancel(fetchData)();
  const userData = await wrapWithCancel(updateUserData)(data);
  const userAddress = await wrapWithCancel(updateUserAddress)(userData);
  const marketingData = await wrapWithCancel(updateMarketingData)(userAddress);

  // because we've wrapped all functions, in case of cancellations
  // we'll just fall through to this point, without calling any of
  // actual functions. We also can't reject by ourselves, since
  // we don't have control over returned promise
  if (cancelled) {
    throw { reason: 'cancelled' };
  }

  return marketingData;

  function wrapWithCancel(fn) {
    return data => {
      if (!cancelled) {
        return fn(data);
      }
    }
  }
}

const token = {};
const promise = updateUser(token);
// wait some time...
token.cancel(); // user will be updated any way

I've written articles, both on cancellation and generators:

To summarize – you have to do some additional work in order to support canncellation, and if you want to have it as a first class citizen in your application, you have to use generators.

Bloomca
  • 1,784
  • 13
  • 17
2

Here is a simple exemple with a promise:

let resp = await new Promise(function(resolve, reject) {
    // simulating time consuming process
    setTimeout(() => resolve('Promise RESOLVED !'), 3000);
    // hit a button to cancel the promise
    $('#btn').click(() => resolve('Promise CANCELED !'));
});

Please see this codepen for a demo

TOPKAT
  • 6,667
  • 2
  • 44
  • 72
1

Using CPromise (c-promise2 package) this can be easily done in the following way (Demo):

import CPromise from "c-promise2";

async function getCdnUrls() {
  console.log(`task1:start`);
  await CPromise.delay(1000);
  console.log(`task1:end`);
}

async function fetchMetaData() {
  console.log(`task2:start`);
  await CPromise.delay(1000);
  console.log(`task2:end`);
}

function* fetchContent() {
  // using generators is the recommended way to write asynchronous code with CPromise
  console.log(`task3:start`);
  yield CPromise.delay(1000);
  console.log(`task3:end`);
}

function* render() {
  console.log(`task4:start`);
  yield CPromise.delay(1000);
  console.log(`task4:end`);
}

const update = CPromise.promisify(function* () {
  var urls = yield getCdnUrls();
  var metadata = yield fetchMetaData(urls);
  var content = yield* fetchContent(metadata);
  yield* render(content);
  return 123;
});

const promise = update().then(
  (v) => console.log(`Done: ${v}`),
  (e) => console.warn(`Fail: ${e}`)
);

setTimeout(() => promise.cancel(), 2500);

Console output:

task1:start 
task1:end 
task2:start 
task2:end 
task3:start 
Fail: CanceledError: canceled 
Dmitriy Mozgovoy
  • 1,419
  • 2
  • 8
  • 7
0

Just like in regular code you should throw an exception from the first function (or each of the next functions) and have a try block around the whole set of calls. No need to have extra if-elses. That's one of the nice bits about async/await, that you get to keep error handling the way we're used to from regular code.

Wrt cancelling the other operations there is no need to. They will actually not start until their expressions are encountered by the interpreter. So the second async call will only start after the first one finishes, without errors. Other tasks might get the chance to execute in the meantime, but for all intents and purposes, this section of code is serial and will execute in the desired order.

Horia Coman
  • 8,681
  • 2
  • 23
  • 25
0

This answer I posted may help you to rewrite your function as:

async function update() {
   var get_urls = comPromise.race([getCdnUrls()]);
   var get_metadata = get_urls.then(urls=>fetchMetaData(urls));
   var get_content = get_metadata.then(metadata=>fetchContent(metadata);
   var render = get_content.then(content=>render(content));
   await render;
   return;
}

// this is the cancel command so that later steps will never proceed:
get_urls.abort();

But I am yet to implement the "class-preserving" then function so currently you have to wrap every part you want to be able to cancel with comPromise.race.

COY
  • 684
  • 3
  • 10
0

I created a library called @kaisukez/cancellation-token

The idea is to pass a CancellationToken to every async function, then wrap every promise in AsyncCheckpoint. So that when the token is cancelled, your async function will be cancelled in the next checkpoint.

This idea came from tc39/proposal-cancelable-promises and conradreuter/cancellationtoken.


How to use my library

  1. Refactor your code
// from this
async function yourFunction(param1, param2) {
    const result1 = await someAsyncFunction1(param1)
    const result2 = await someAsyncFunction2(param2)
    return [result1, result2]
}

// to this
import { AsyncCheckpoint } from '@kaisukez/cancellation-token'
async function yourFunction(token, param1, param2) {
    const result1 = await AsyncCheckpoint.after(token, () => someAsyncFunction1(param1))
    const result2 = await AsyncCheckpoint.after(token, () => someAsyncFunction2(param2))
    return [result1, result2]
}
  1. Create a token then call your function with that token
import { CancellationToken, CancellationError } from '@kaisukez/cancellation-token'

const [token, cancel] = CancellationToken.source()

// spawn background task (run async function without using `await`)
CancellationError.ignoreAsync(() => yourAsyncFunction(token, param1, param2))

// ... do something ...

// then cancel the background task
await cancel()

So this is the solution of the OP's question.

import { CancellationToken, CancellationError, AsyncCheckpoint } from '@kaisukez/cancellation-token'

async function update(token) {
   var urls = await AsyncCheckpoint.after(token, () => getCdnUrls());
   var metadata = await AsyncCheckpoint.after(token, () => fetchMetaData(urls));
   var content = await AsyncCheckpoint.after(token, () => fetchContent(metadata));
   await AsyncCheckpoint.after(token, () => render(content));
   return;
}

const [token, cancel] = CancellationToken.source();

// spawn background task (run async function without using `await`)
CancellationError.ignoreAsync(() => update(token))

// ... do something ...

// then cancel the background task
await cancel()
kaisukez
  • 91
  • 5
-1

Example written in Node with Typescript of a call which can be aborted from outside:

function cancelable(asyncFunc: Promise<void>): [Promise<void>, () => boolean] {
  class CancelEmitter extends EventEmitter { }

  const cancelEmitter = new CancelEmitter();
  const promise = new Promise<void>(async (resolve, reject) => {

    cancelEmitter.on('cancel', () => {
      resolve();
    });

    try {
      await asyncFunc;
      resolve();
    } catch (err) {
      reject(err);
    }

  });

  return [promise, () => cancelEmitter.emit('cancel')];
}

Usage:

const asyncFunction = async () => {
  // doSomething
}

const [promise, cancel] = cancelable(asyncFunction());

setTimeout(() => {
  cancel();
}, 2000);

(async () => await promise)();
hya
  • 1,708
  • 2
  • 15
  • 22