• How to Cancel Your Promise
  • Originally written by Seva Zaikov
  • The Nuggets translation Project
  • Permanent link to this article: github.com/xitu/gold-m…
  • Translator: jonjia
  • Proofreader: kangkai124 hexianga

How do YOU cancel your Promise?

In the ES6 version of ECMAScript, the international standard for the JavaScript language, a new asynchronous native object, Promise, has been introduced. This is a very powerful concept that allows us to avoid the infamous callback trap. For example, several asynchronous operations could easily be written as follows:

function updateUser(cb) {
  fetchData(function(error, data) => {
    if (error) {
      throw error;
    }
    updateUserData(data, function(error, data) => {
      if (error) {
        throw error;
      }
      updateUserAddress(data, function(error, data) => {
        if (error) {
          throw error;
        }
        updateMarketingData(data, function(error, data) => {
          if (error) {
            throw error;
          }

          // finally!
          cb();
        });
      });
    });
  });
}

Copy the code

As you can see, we have several nested callbacks, and it would be difficult to manage the code if we wanted to change the order of some callbacks, or if we wanted to execute several at the same time. However, with Promise, we can refactor it into a more readable version:

// We don't need the callback anymore -- just use itthenMethod // handles the return result of the functionfunction updateUser() {
  return fetchData()
    .then(updateUserData)
    .then(updateUserAddress)
    .then(updateMarketingData);
}

Copy the code

Not only is this code cleaner and more readable, it can easily switch the order of callbacks, execute them at the same time or remove unnecessary callbacks (or add a callback in the middle of the callback chain).

One drawback of using the Promise chain is that we don’t have access to the scope (or unreturned variables) of each callback function. You can read Dr. Alex Rauschmayer’s a Great article to address this problem.

However, I discovered that you can’t cancel promises, and this is a key issue. Sometimes you need to cancel promises, and you need to build workarounds — depending on how often you use the feature.

Use the Bluebird

Bluebird is a Promise implementation library that is fully compatible with native Promise objects and adds some useful methods on top of the prototype Promise. Prototype. Here we’ll just cover the cancel method, which partially implements what we want – it allows us to have custom logic when we cancel a promise with promise.cancel (why partial implementation? Because the code is verbose and not generic).

In our example, let’s see how Bluebird can be used to cancel a Promise:

import Promise from 'Bluebird';

function updateUser() {
  return new Promise((resolve, reject, onCancel) => {
    let cancelled = false; // You need to change Bluebird configuration, To use the cancellation feature / / http://bluebirdjs.com/docs/api/promise.config.html onCancel (() = > {cancelled =true;
      reject({ reason: 'cancelled' });
    });

    return fetchData()
      .then(wrapWithCancel(updateUserData))
      .then(wrapWithCancel(updateUserAddress))
      .then(wrapWithCancel(updateMarketingData))
      .then(resolve)
      .catch(reject);

    functionWrapWithCancel (fn) {// Promise Resolved state requires only one argument to be passedreturn (data) => {
        if(! cancelled) {returnfn(data); }}; }}); } const promise = updateUser(); // Wait a minute... promise.cancel(); // The user will still be updatedCopy the code

As you can see, we added a lot of code to the previous clean example. Unfortunately, there is no other way, because we can’t stop executing a random Promise chain (we need to wrap it in another function if we want to), so we need to wrap each callback function with a function that handles cancellation state.

Pure Promises

The above technology is not what makes Bluebird special, it’s more about the interface – you can implement your own cancelled version but require additional properties/variables. Usually this method is called cancellationToken, and in essence, it’s almost the same as the previous one, but instead of having this method on promise.prototype.cancel, we instantiate it on a different object – we can return an object with the cancel property, Or we can take an extra parameter, an object, where we’ll add a property.

function updateUser() {
  let resolve, reject, cancelled;
  const promise = new Promise((resolveFromPromise, rejectFromPromise) => {
    resolve = resolveFromPromise;
    reject = rejectFromPromise;
  });

  fetchData()
    .then(wrapWithCancel(updateUserData))
    .then(wrapWithCancel(updateUserAddress))
    .then(wrapWithCancel(updateMarketingData))
    .then(resolve)
    .then(reject);

  return {
    promise,
    cancel: () => {
      cancelled = true;
      reject({ reason: 'cancelled'}); }};function wrapWithCancel(fn) {
    return (data) => {
      if(! cancelled) {returnfn(data); }}; } } const { promise, cancel } = updateUser(); // Wait a minute... cancel(); // The user will still be updatedCopy the code

This is a little more verbose than previous solutions, but it addresses the same problem and is a viable solution if you’re not using Bluebird (or don’t want to use a non-standard approach with Promise). As you can see, we changed the signature – now we return the object instead of a Promise, but we can actually pass an object argument to the function with the cancel method attached (or the Monkey-patch instance of the Promise, but that will also cause you problems later). If you only have this requirement in a few places, this is a good solution.

Switch to the generators

Generators are another new ES6 feature, but for some reason they are not widely used. Think before you use it – will the newbies on your team not understand it, or will all of them be able to do it? Also, it exists in some other languages, such as Python, so it should be easy to use this solution as a team.

Generators have its own documentation, so I won’t cover the basics, just implement a Generator executor that will allow us to cancel our promises in a generic way without affecting our code.

// This is the core method to run our asynchronous code and provide the Cancellation methodfunctionrunWithCancel(fn, ... args) { const gen = fn(... args);letcancelled, cancel; Const promise = new Promise((resolve, promiseReject) => {// Define the cancel method and return it cancel = () => {cancelled =true;
      reject({ reason: 'cancelled' });
    };

    let value;

    onFulfilled();

    function onFulfilled(res) {
      if(! cancelled) {let result;
        try {
          result = gen.next(res);
        } catch (e) {
          return reject(e);
        }
        next(result);
        returnnull; }}function onRejected(err) {
      var result;
      try {
        result = gen.throw(err);
      } catch (e) {
        return reject(e);
      }
      next(result);
    }

    function next({ done, value }) {
      if (done) {
        returnresolve(value); } // Assume we always accept promises, so we don't need to check the typereturnvalue.then(onFulfilled, onRejected); }});return { promise, cancel };
}
Copy the code

This is a pretty long function, but basically it (except for checking, which of course is a very rudimentary implementation) – the code itself will stay exactly the same and we’ll get the cancel method literally! Let’s see how to use it in our example:

// you can put * almost anywhere :) // this is syntactically similar to async/awaitfunction* updateUser() {// Assume that all of our functions return Promise // otherwise we need to adjust our executor function // to accept Generator const data = yield fetchData(); const userData = yield updateUserData(data); const userAddress = yield updateUserAddress(userData); const marketingData = yield updateMarketingData(userAddress);returnmarketingData; } const { promise, cancel } = runWithCancel(updateUser); // Cancel ();Copy the code

As you can see, the interface remains the same, but we now have the option to cancel any generator-based functions during execution, simply by wrapping them in the appropriate runner. The downside is consistency – if it’s only in a few places in your code, it can be confusing for others to look at your code because you’re using every possible asynchronous method in your code, another compromise.

I think Generator is the most scalable option because you can literally do everything you want – if something happens you can pause, wait, retry, or run another Generator. However, I don’t see them very often in JavaScript code, so you should consider adoption and cognitive load – do you really have a lot of usage scenarios for it? If so, then it’s a very good solution that you may thank yourself for in the future.

Pay attention to the async/await

In ES2017 version async/await is provided and you can use them without any flags in Node.js (after version 7.6). Unfortunately, there’s nothing to support canceling promises, and since async functions implicitly return promises, we can’t really feel it (attach a property or return something else), just the resolved/ Rejected state value. This means that in order for our function to be cancelled, we need to pass an object and wrap each call in our famous wrapper method:

async function updateUser(token) {
  let cancelled = false; // We do not call reject because we cannot access the Promise returned by // we do not call other functions // Call reject token.cancel = () => {cancelled = at the endtrue; }; const data = await wrapWithCancel(fetchData)(); const userData = await wrapWithCancel(updateUserData)(data); const userAddress = await wrapWithCancel(updateUserAddress)(userData); const marketingData = await wrapWithCancel(updateMarketingData)(userAddress); // Since we've wrapped all the functions in case of cancellation // we don't need to call any actual functions to do this // we also can't call the reject method // because we have no control over the Promise returnedif (cancelled) {
    throw { reason: 'cancelled' };
  }

  return marketingData;

  function wrapWithCancel(fn) {
    return data => {
      if(! cancelled) {returnfn(data); } } } } const token = {}; const promise = updateUser(token); // Wait a minute... token.cancel(); // The user will still be updatedCopy the code

This is a very similar solution, but because we didn’t call reject directly in the Cancel method, it might confuse the reader. On the other hand, it is a standard feature of today’s languages, has a very convenient syntax, allows you to use the results of previous calls later (so the Promise chain-call problem is solved here), and has very concise and intuitive error handling through try/catch. So, if cancellation doesn’t bother you anymore (or if you can cancel something this way), this feature is definitely the best way to write asynchronous code in modern JavaScript.

Use Streams (like RxJS)

Streams is a completely different concept, but it’s actually more widely used than JavaScript, so you can think of it as a platform-independent model. Streams could be better or worse than Promie/Generator. If you’ve been exposed to it and used it for some (or all) asynchronous logic, you’ll find Streams better, and if you haven’t, you’ll find Streams worse because it’s a completely different approach.

I’m not an expert in using Streams, just a few, and I think you should use them for all asynchronous events, or none at all. So if you already use them, this shouldn’t be a problem for you, as this is a well-known feature of the Streams library for a long time.

As I mentioned, I don’t have enough experience with Streams to provide a solution for using them, so I’ll just post a few links about Streams implementation cancellations:

  • Making issue explain
  • Articles on using * methods

accept

Things are moving in the right direction – FETCH will add abort, and how to cancel promises will be hotly debated for a long time to come. Can canceling promises be fulfilled? Maybe yes, maybe no. Also, cancelling promises isn’t crucial for many applications – yes, you can make a few additional requests, but it’s very rare to have more than one result. Also, if it happens once or twice, you can use extended examples from the outset to address these specific functions. However, if you have a lot of these situations in your application, consider what is listed above.


The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.