Skip to content

Instantly share code, notes, and snippets.

@jakearchibald
Last active February 2, 2016 10:25
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jakearchibald/9a24f3c06f06b9c06a1e to your computer and use it in GitHub Desktop.
Save jakearchibald/9a24f3c06f06b9c06a1e to your computer and use it in GitHub Desktop.

Options for cancelling promises

Requirements from fetch

// fetch returns a promise
var fp = fetch(url);
  • Developers should be able to cancel the network request triggered by fetch.
  • If the fetch operation is cancelled before fp settles, the promise should settle in a way that signals cancelation.
  • If the fetch operation is cancelled before fp settles, but completes before it's able to cancel, fp should still cancel.
var fp = fetch(url);
// r.json streams the response body from the network,
// parsing it as json
var jp = fp.then(r => r.json());
  • Developers should be able to cancel the whole operation of jp, meaning if fp has yet to settle, the fetch is cancelled.
// fetch user data & store the response
var getUserData = (function() {
  var fp;
  return function() {
    if (!fp) {
      fp = fetch('//user-data-url').catch(err => {
        fp = undefined;
        throw err;
      });
    }
    return fp.then(r => r.clone());
  }
}());

var jp = getUserData().then(r => r.json());
var tp = getUserData().then(r => r.text());
  • Developers should be able to cancel jp, which would prevent/cancel the stream-read-as-JSON if it hasn't already completed, but it shouldn't affect the resolution of tp.
  • Developers should be able to cancel fp, which if not-settled, would result in jp and tp rejecting with undefined.
  • Developers should be able to cancel both jp and tp, fp should be able to react to this loss of interest by also cancelling.
// in a ServiceWorker
self.addEventListener('fetch', event => {
  event.respondWith(
    fetch(url1).then(r => r.json()).then(data => {
      return fetch(data.url2);
    })
  );
});
  • The browser should be able to signal loss-of-interest in the promise passed to respondWith, resulting in the cancelation of the parent-most unsettled promise.

Finally

Finally should be added to regular promises:

Promise.reject(Error("Boo")).finally(_ => {
  console.log("Finally!");  
}).then(_ => {
  console.log("Yey");
});
// logs: "Finally!", does not log "Yey"

The return value from finally has no impact, but multiple finallys can be applied:

Promise.resolve().then(_ => {
  console.log("Resolved 1");
  throw Error("Boo");
}).finally(_ => {
  console.log("Finally 1");
}).finally(_ => {
  console.log("Finally 2");
}).then(_ => {
  console.log("Resolved 2");
}, err => {
  console.log("Rejected 1");
}).finally(_ => {
  console.log("Finally 3");
});
// logs: "Resolved 1", "Finally 1", "Finally 2", "Rejected 1", "Finally 3"

This is useful for always-actions, such as hiding spinners.

Cancelable promise design

This is an iteration on the proposal in the cancelable fetch centithread clustermisery.

Cancelability uses a ref-counting approach, where the references are based on promise-chain dependency:

var fetchPromise = fetch(url);
// Ref counts
// fetchPromise: 0

var jsonPromise = fetchPromise.then(response => {
  var innerJSONPromise = response.clone().json();
  return innerJSONPromise;
});
// Ref counts
// fetchPromise: 1
// jsonPromise:  0

var textPromise = fetchPromise.then(response => {
  var innerTextPromise = response.clone().text();
  return innerTextPromise;
});

var textTrimPromise = textPromise.then(t => t.trim()).finally(_ => console.log("Finally!"));
// Ref counts
// fetchPromise:    2
// jsonPromise:     0
// textPromise:     1
// textTrimPromise: 0

Then, assuming that fetchPromise has not settled:

textTrimPromise.cancel();
  1. textTrimPromise is marked for cancelation.
  2. textTrimPromise's parent, textPromise has its ref count decreased by 1, to 0.
  3. textPromise is marked for cancelation.
  4. textPromise's parent, fetchPromise has its ref count decreased by 1, to 1.
  5. textPromise, the parent-most in the chain marked for cancelation, immediately cancels (does not wait for fetchPromise to resolve).

When a cancelable promise is canceled, neither its onFulfil or onReject handlers are called, but its onFinally handler will call. All child promises are also canceled. In the above code, "Finally" will be logged.

Then:

jsonPromise.cancel();
  1. jsonPromise is marked for cancelation.
  2. jsonPromise's parent, fetchPromise has its ref count decreased by 1, to 0.
  3. fetchPromise is marked for cancelation.
  4. fetchPromise, is the parent-most in the chain marked for cancelation. As its work has begun, it gets to react to this cancelation and abort the underlying stream.

Handling promises resolved with promises

var outerFetchPromise = fetch(url);
var resultPromise = outerFetchPromise.then(response => {
  var jsonPromise = response.json();
  var innerFetchPromise = jsonPromise.then(data => fetch(data.url));
  var textPromise = innerFetchPromise.then(r => r.text());
  return textPromise;
});

Assuming outerFetchPromise has resolved with a Response, and jsonPromise has resolved with an object, our ref-counts are:

// resultPromise:     0
// innerFetchPromise: 1
// textPromise:       1

textPromise has a ref-count of 1 because it's the resolved value of resultPromise.

Then:

resultPromise.cancel();
  1. resultPromise cannot be marked for cancelation as it has resolved. But since it hasn't settled, and its resolved value, textPromise, is a cancelable promise, it has its ref count decreased by 1, to 0.
  2. textPromise is marked for cancelation
  3. textPromise's parent, innerFetchPromise has its ref count decreased by 1, to 0.
  4. innerFetchPromise is marked for cancelation.
  5. innerFetchPromise, is the parent-most in the chain marked for cancelation. As its work has begun, it gets to react to this cancelation and abort the underlying request.

A gotcha with resolved-but-unsettled promises:

Take the following nonsensical example:

var fetchPromise = fetch(url);
var jsonPromise = fetchPromise.then(r => r.json());
var fetchPromise2 = fetch(url2);
var waitingPromise = fetchPromise2.then(_ => fetchPromise);
// Ref counts
// fetchPromise:   2
// jsonPromise:    0
// waitingPromise: 0

Imagine fetchPromise has yet to settle, fetchPromise2 has fulfilled with a Response, waitingPromise has resolved with fetchPromise. Then:

waitingPromise.cancel();
  1. waitingPromise cannot be marked for cancelation as it has resolved. But since it hasn't settled, its resolved value, fetchPromise has its ref count decreased by 1, to 1.

And there's the problem. Although .cancel was called on this unsettled promise, it continues to resolve with fetchPromise.

This wouldn't be a problem with:

var fetchPromise = fetch(url);
var jsonPromise = fetchPromise.then(r.clone() => r.json());
var fetchPromise2 = fetch(url2);
var waitingPromise = fetchPromise2.then(_ => fetchPromise).then();
// Ref counts
// fetchPromise:   2
// jsonPromise:    0
// waitingPromise: 0

…as waitingPromise hasn't resolved due to the added .then(). I think we either need to find a way to do something like that in the spec (maybe it does already?), or allow a resolved but unsettled promise to cancel. Both of these would also help if a CancelablePromise resolved with a foreign thenable.

I'm having trouble getting my head around this bit of the spec, so there may be a simpler answer.

Creating a cancelable promise

var p = new CancelablePromise((resolve, reject) => {
  var timerID = setTimeout(_ => resolve("Done!"), 5000);

  return function onCancel() {
    clearTimeout(timerID);
  });
});

The function returned from CancelablePromise's first argument is called if this promise is cancelled before settling, but after onCancel was called.

Question: If the function passed to onCancel calls resolve or reject synchronously, should this value become the settled value? Since we've switched to the .finally approach, I think not.

Comparison with the token approach

Creating and cancelling a promise

// Cancelable promises
var p = new CancelablePromise((resolve, reject, onCancel) => {
  var timerID = setTimeout(_ => resolve("Done!"), 5000);

  return _ => {
    clearTimeout(timerID);
  });
});
p.cancel();
// Cancelable token
var cts = new CancellationTokenSource();
var p = new Promise((resolve, reject) => {
  var timerID = setTimeout(_ => resolve("Done!"), 5000);

  cts.token.register(_ => {
    clearTimeout(timerID);
  });
});
cts.cancel();

Canceling a promise chain

// Cancelable promises

startSpinner();
var p = fetch(url)
  .then(r => r.json())
  .then(data => fetch(data.url))
  .then(r => text())
  .then(text => updateUI(text))
  .catch(err => showUIError())
  .finally(stopSpinner);
p.cancel();
// Cancelable token
startSpinner();
var cts = new CancellationTokenSource();
var p = fetch(url, { cancelationToken: cts.token })
  .then(r => r.json({ cancelationToken: cts.token }))
  .then(data => fetch(data.url, { cancelationToken: cts.token }))
  .then(r => text({ cancelationToken: cts.token }))
  .then(text => updateUI(text))
  .catch(err => showUIError(err))
  .finally(stopSpinner);
cts.cancel();
stopSpinner();

Note: I'm assuming with the token approach, it'll leave my promise chain in a pending state, so I'll have to call stopSpinner() post-cancelation. I'm also relying on the canceled item not to call reject() with some kind of abort error. I'll assume that leaving promises pending is the convention & everything in the chain does that.

Canceling multiple things at once

// Cancelable promises
var p = CancelablePromise.all(
  [url1, url2, url3].map(url => {
    return fetch(url).then(r => r.json());
  })
)

p.cancel();
// Cancelable token
var cts = new CancellationTokenSource();
var p = Promise.all(
  [url1, url2, url3].map(url => {
    return fetch(url, { cancelationToken: cts.token })
      .then(r => r.json({ cancelationToken: cts.token }));
  })
)

cts.cancel();

Canceling parent once all children have cancelled

// Cancelable promises
var getUserData = (function() {
  var fp;
  return function() {
    if (!fp) {
      fp = fetch('//user-data-url').catch(err => {
        fp = undefined;
        throw err;
      });
    }
    return fp.then(r => r.clone());
  }
}());

var jp = getUserData().then(r => r.json());
var tp = getUserData().then(r => r.text());

jp.cancel();
tp.cancel();
// Cancelable token
var getUserData = (function() {
  var fp;
  var tokenSet = new Set();
  var cts;
  return function({ cancelationToken }) {
    if (!fp) {
      cts = new CancellationTokenSource();
      tokenSet.clear();
      fp = fetch('//user-data-url', {
        cancelationToken: cts.token
      }).catch(err => {
        fp = undefined;
        throw err;
      });
    }

    // Using a set because multiple calls with the
    // same cancelationToken should count at one.
    if (cancelationToken && !tokenSet.has(cancelationToken)) {
      cancelationToken.register(_ => {
        tokenSet.remove(cancelationToken);
        if (tokenSet.size === 0) {
          cts.cancel();
        }
      });
    }

    // If this is called without a cancelationToken,
    // it should still increase the 'refcount'
    tokenSet.add(cancelationToken || new CancellationTokenSource().token);

    return fp.then(r => r.clone());
  }
}());

var cts1 = new CancellationTokenSource();
var jp = getUserData({ cancelationToken: cts1.token }).then(r => r.json({ cancelationToken: cts1.token }));
var cts2 = new CancellationTokenSource();
var tp = getUserData({ cancelationToken: cts2.token }).then(r => r.text({ cancelationToken: cts2.token }));

cts1.cancel();
cts2.cancel();

Keeping cancelation to yourself

// Cancelable promises
var getUserData = (function() {
  var fp;
  return function() {
    if (!fp) {
      fp = fetch('//user-data-url').catch(err => {
        fp = undefined;
        throw err;
      });
    }
    return Promise.resolve(fp.then(r => r.clone()));
  }
}());

Question: Should there be an easier way to become uncancelable than Promise.resolve(cancelablePromise)? cancelablePromise.protect() could be a shortcut.

// Cancelable token
var getUserData = (function() {
  var fp;
  var cts;
  return function() {
    if (!fp) {
      cts = new CancellationTokenSource();
      fp = fetch('//user-data-url', {
        cancelationToken: cts.token
      }).catch(err => {
        fp = undefined;
        throw err;
      });
    }
    return fp.then(r => r.clone());
  }
}());

Giving the promise-receiver cancelation ability

// Cancelable promises
self.addEventListener('fetch', event => {
  event.respondWith(
    fetch(url1).then(r => r.json()).then(data => {
      return fetch(data.url2);
    })
  );
});
// Cancelable token
self.addEventListener('fetch', event => {
  var cts = new CancellationTokenSource();
  event.respondWith(
    fetch(url1, {
      cancelationToken: cts.token
    }).then(r => r.json({
      cancelationToken: cts.token
    })).then(data => {
      return fetch(data.url2, {
        cancelationToken: cts.token
      });
    }),
    { canceller: cts }
  );
});
@domenic
Copy link

domenic commented Apr 2, 2015

Looking good. A few thoughts:

  • Probably should show an example of how to cancel multiple fetches at once. I think that will favor cancellation tokens.
  • This doesn't explain how promises-that-get-resolved-to-cancelable-promises work. The last example implies that returning a cancellable promise from inside another cancellable promise's onFulfilled handler will work, somehow. But I don't understand how. Also, what about resolving non-cancelable promises with cancelable promises.
  • Rejecting with undefined is not happy-making. I see two plausible alternatives:
    • Given your onCanceled design, I'd say by default cancelation does nothing, and if the creator of the promise wants it to do something, they call resolve or reject.

    • Alternately... some kind of thing where we add Promise.prototype.finally (with the usual semantics), but also say that cancellation can be caught by a finally but not by a catch or then? This kind of fits with generators' return method, and with analogous sync scenarios like

      function foo() {
        try {
          doStuff();
      
          if (condition) {
            // "cancel" by returning early
            return;
          }
      
          doMoreStuff();
        } catch (e) {
          // analogous to onRejected code
          // "cancel"ing doesn't trigger this
        } finally {
          console.log("was 'canceled'!");
        }
      
        // analogous to onFulfilled code
        // "cancel"ing doesn't trigger this
      }

      I'm not sure whether this is workable. If it is I'm not sure whether we make Promise.prototype.finally able to catch cancellations, or only CancelablePromise.prototype.finally. (Maybe it doesn't matter.)

  • onCancelled(fn) seems awkward. Maybe just return fn.
  • Digging a bit into implementation, I think we'd want to formalize the ref-count stuff by saying that CancelablePromise.prototype.then returns a new CancelablePromise whose onCanceled decreases the ref count.
  • More generally, implementation-wise, I think we need to start creating a prototype implementation. Chrome Canary might allow you to subclass promises by now. (Although their nonstandard .chain bullshit might cause problems; perhaps just using es6-promise would be better.)

@jakearchibald
Copy link
Author

Probably should show an example of how to cancel multiple fetches at once.

Added.

This doesn't explain how promises-that-get-resolved-to-cancelable-promises work.

I've added this, and it unearthed what could be a gotcha. I couldn't quite get my head around that bit of the spec, so it may not be a big deal. Could you review it?

what about resolving non-cancelable promises with cancelable promises

Promise.resolve(cancellablePromise) should increase cancellablePromise's ref count and return a regular promise.

Given your onCanceled design, I'd say by default cancelation does nothing, and if the creator of the promise wants it to do something, they call resolve or reject.

This is problematic with the first example - textPromise rejects without either of its handlers being called.

Alternately... some kind of thing where we add Promise.prototype.finally

Ohhh, I totally forgot about the return behaviour. Yes, this is the right thing to do here. Have updated the doc.

onCancelled(fn) seems awkward. Maybe just return fn.

Hmm, ok, but that doesn't feel much less awkward to me. You lose the naming.

@domenic
Copy link

domenic commented Apr 9, 2015

I've added this, and it unearthed what could be a gotcha. I couldn't quite get my head around that bit of the spec, so it may not be a big deal. Could you review it?

Yeah, this is interesting. Going to have to prototype and play around to see if there's a solution; too hard to abstractly reason about.

Promise.resolve(cancellablePromise) should increase cancellablePromise's ref count and return a regular promise.

OK cool, I think that just works because Promise.resolve will treat cancelablePromise as a foreign thenable and call .then on it.

This is problematic with the first example - textPromise rejects without either of its handlers being called.

Yeah, with finally I think we have a better strategy.

Hmm, ok, but that doesn't feel much less awkward to me. You lose the naming.

I find onCanceled(fn) awkward because it's so unlike resolve and reject.

An alternate design, if we don't think cancel handlers should be able to resolve or reject, is new CancelablePromise(executor, onCanceled) or similar.

@rbuckton
Copy link

rbuckton commented Apr 9, 2015

In regards to a cancellation token, I was thinking of a more integrated API:

class Promise {
  constructor(init: (resolve: (value?: any) => void, reject: (reason?: any) => void) => void, token?: CancellationToken);
  then(token?: CancellationToken): Promise;
  then(onFulfilled: (value: any) => any, token?: CancellationToken): Promise;
  then(onFulfilled: (value: any) => any, onRejected: (reason?: any) => any, token?: CancellationToken): Promise;
  then(onFulfilled: (value: any) => any, onRejected?: (reason?: any) => any, onFinally?: () => any, token?: CancellationToken): Promise;
  catch(onRejected: (reason?: any) => any, token?: CancellationToken): Promise;
  catch(onRejected: (reason?: any) => any, onFinally: () => any, token?: CancellationToken): Promise;
  finally(onFinally: () => any, token?: CancellationToken): Promise;

  static resolve(value: any, token?: CancellationToken): Promise;
  static reject(reason: any, token?: CancellationToken): Promise;
  static all(promises: any[], token?: CancellationToken): Promise;
  static race(promises: any[], token?: CancellationToken): Promise;
}

class CancellationTokenSource {
    constructor(...linkedTokens: CancellationToken[]);
    token: CancellationToken;
    cancel(): void;
}

class CancellationToken {
    static none: CancellationToken;
    canceled: boolean;
    register(onCanceled: () => void): { unregister(): void; }    
}

The same ref counting could be leveraged internally in a Promise chain through the use of linked CancellationTokenSource objects. If that were the case, your promise chain example would instead read:

startSpinner();
let cts = new CancellationTokenSource();
let p = fetch(url, cts.token)
  .then(r => r.json())
  .then(data => fetch(data.url))
  .then(r => r.text())
  .then(text => updateUI(text))
  .catch(err => showUIError())
  .finally(stopSpinner);
cts.cancel();

This would allow for both the scenarios you have outlined above, as well as providing an API that can work well with async functions:

async function getData(url, token = CancellationToken.none) {
  startSpinner();
  try {
    let r0 = await fetch(url, token);
    let data = await r0.json(token);
    let r1 = await fetch(data.url, token);
    let text = await r1.text(token);
    updateUI(text);
  }
  catch (err) {
    showUIError(err);
  }
  finally {
    stopSpinner();
  }
}

let cts = new CancellationTokenSource();
getData(url, cts.token);
cts.cancel();

And affords other possible use cases:

async function showStockTicker(token = CancellationToken.none) {
  while (!token.canceled) {
    let stocks = await getTickerUpdates(token);
    updateUI(stocks);
  }
}

let cts = new CancellationTokenSource();
showStockTicker(cts.token);

// stop receiving ticker updates after 1 minute
setTimeout(() => cts.cancel(), 600000);

@rbuckton
Copy link

rbuckton commented Apr 9, 2015

Another possible option for the token approach, is to expose a method on CancellationTokenSource to create linked sources:

class CancellationTokenSource {
    constructor(...linkedTokens: CancellationToken[]);
    token: CancellationToken;
    cancel(): void;
    link(token?: CancellationToken): CancellationTokenSource;
}

Which would simplify the logic for Canceling a parent once all children have canceled scenario for tokens:

var root = new CancellationTokenSource();
var getUserData = (function() {
  var fp;
  return function() {
    if (!fp) {
      fp = fetch('//user-data-url', root.token).catch(err => {
        fp = undefined;
        throw err;
      });
    }
    return fp.then(r => r.clone());
  }
}());

var jpcts = root.link();
var jp = getUserData().then(r => r.json(jpcts.token));

var tpcts = root.link();
var tp = getUserData().then(r => r.text(tpcts.token));

jpcts.cancel();
tpcts.cancel();

@rbuckton
Copy link

rbuckton commented Apr 9, 2015

Also, here is an alternate approach to the Giving the promise receiver cancellation ability, where the receiver provides the token:

// Cancelable token
self.addEventListener('fetch', event => {
  // event contains a `cancellationToken` property with the token provided by the "fetch" event.
  let cancellationToken = event.cancellationToken;
  event.respondWith(
    fetch(url1, { cancelationToken })
      .then(r => r.json({ cancelationToken }))
      .then(data => fetch(data.url2, { cancelationToken }))
  );
});

Out of curiosity, is there a reason that all uses of a cancellation token in your samples use such a verbose form: { cancellationToken: cts.token }? If tokens were an integral part of cancellation, I would anticipate them being a regular argument rather than part of an options bag, though that would obviously not be required.

@jakearchibald
Copy link
Author

@rbuckton a more integrated approach with the tokens is much better, I hadn't seen that on any of the lists. Is there a more concrete proposal for this approach?

Out of curiosity, is there a reason that all uses of a cancellation token in your samples use such a verbose form: { cancellationToken: cts.token }?

It's pretty standard for optional params to end up in an options object. Avoids functionName("Hello", null, null, null, true) etc.

@jakearchibald
Copy link
Author

Bluebird 3.0 cancelable promises are reassuringly close to this design http://bluebirdjs.com/docs/api/cancellation.html

Taking one of our gotchas:

var promise1 = Promise.resolve("Hello");
var promise2 = wait(4000, "World"); // a promise that resolves with "World" in 4000ms

promise2.then(value => {
  console.log(value);
});

var promise1_1 = promise1.then(val => promise2);

promise1.then(_ => {
  promise1_1.cancel();
});

…the above results in promise2 being cancelled, because val => promise2 doesn't up the dependency count on promise2. I don't think we want to duplicate this pattern.

If a cancellable then/catch callback returns a cancellable promise, we should do an implicit .then() on it, increasing the returned promise's refcount by 1. So…

var promise1 = Promise.resolve("Hello");
var promise2 = wait(4000, "World"); // a promise that resolves with "World" in 4000ms

promise2.then(value => {
  console.log(value);
});

var promise1_1 = promise1.then(val => {
  return promise2;
  // refcounts at this point in execution:
  // promise1:   resolved
  // promise2:   2
  // promise2-implicit-then: 1
  // promise1_1: 0
}).then(_ => {
  // this never happens
}, _ => {
  // this never happens
}).finally(_ => {
  // this happens!
});

promise1.then(_ => {
  promise1_1.cancel();
  // The chain is walked up, decreasing refcounts from 1 to 0 and marking for cancellation.
  // When it gets to the promise resolved with promise2's implicit then, it continues up that chain.
  // promise2's implicit then is marked for cancelation.
  // Its parent, promise2, as its refcount decreased from 2 to 1.
  // refcounts at this point in execution:
  // promise1:   resolved
  // promise2:   1
  // promise2-implicit-then: canceled
  // promise1_1: canceled
});

// refcounts at this point in execution:
// promise1:   2
// promise2:   1
// promise1_1: 0

@jakearchibald
Copy link
Author

Another interesting Bluebird behaviour:

var p1 = Promise.resolve();
p1.cancel();
var p2 = p1.then();

p2 is rejected with a cancelation error, because p1 has already cancelled. I'm not particularly fond of that, but am digging into it…

It leaves you with this:

p.then(v => v).catch(errback);
p.cancel();
p.then(v => v).catch(anotherErrback);

…it's possible for errBack not to be called, due to p canceling, but anotherErrback will be called with a cancelation error. Hm.

@jed
Copy link

jed commented Sep 23, 2015

lest we end up with another referer debacle, we should probably pick a side in the Canceled vs. cancelled debate.

Here are the current stats on this page:

Cancellation: |||||||||||||||||||||||||||||||||||||||||||||||||||||| (54)
Cancelation: ||||||||||||||||||||||||||||||||||||||||||||||||| (49)

@machty
Copy link

machty commented Sep 23, 2015

@jed could you open an RFC at https://github.com/english/rfcs ?

@jakearchibald
Copy link
Author

Hah, well it should be CancelablePromise due to existing APIs like https://developer.mozilla.org/en-US/docs/Web/API/Event/cancelable - that was already decided and I did a find/replace in the OP.

As for CancellationToken vs CancelationToken - I don't really care, they were only used as a comparison of the method, I could have called them ThatllDoPigToken. "Cancelation" is uncommon outside the US, but whatever. Although my personal writings will remain en-gb.

@dlindahl
Copy link

The spelling distinction extends to cancelers and cancellers, as well as to cancelable and cancellable, but it does not not extend to cancellation, which everywhere is spelled with two l’s.
-- http://grammarist.com/spelling/cancel/

"canceled" is supposed to be a US thing whereas "cancelled" is a British imperial thing. But I've had a similar discussion on a previous dev team and found that "cancelled" seems to be used more colloquially in the US (in a purely unscientific study of things I've noticed with a certain degree of selection bias)

There are dozens of blog posts about this: https://www.google.com/search?q=canceled+vs+cancelled&oq=canceled+vs+cancelled&aqs=chrome..69i57.6512j0j7&sourceid=chrome&es_sm=91&ie=UTF-8. Its likely a un-winnable argument so if there is already a precedent (as in the case of the previously linked Event API), then it is probably safer to go with that form.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment