javascriptpromisecancellation

Promise - is it possible to force cancel a promise


I use ES6 Promises to manage all of my network data retrieval and there are some situations where I need to force cancel them.

Basically the scenario is such that I have a type-ahead search on the UI where the request is delegated to the backend has to carry out the search based on the partial input. While this network request (#1) may take a little bit of time, user continues to type which eventually triggers another backend call (#2)

Here #2 naturally takes precedence over #1 so I would like to cancel the Promise wrapping request #1. I already have a cache of all Promises in the data layer so I can theoretically retrieve it as I am attempting to submit a Promise for #2.

But how do I cancel Promise #1 once I retrieve it from the cache?

Could anyone suggest an approach?


Solution

  • In modern JavaScript - no

    Promises have settled (hah) and it appears like it will never be possible to cancel a (pending) promise.

    Instead, there is a cross-platform (Node, Browsers etc) cancellation primitive as part of WHATWG (a standards body that also builds HTML) called AbortController. You can use it to cancel functions that return promises rather than promises themselves:

    // Take a signal parameter in the function that needs cancellation
    async function somethingIWantToCancel({ signal } = {}) {
      // either pass it directly to APIs that support it
      // (fetch and most Node APIs do)
      const response = await fetch('.../', { signal });
      // return response.json;
    
      // or if the API does not already support it -
      // manually adapt your code to support signals:
      const onAbort = (e) => {
        // run any code relating to aborting here
      };
      signal.addEventListener('abort', onAbort, { once: true });
      // and be sure to clean it up when the action you are performing
      // is finished to avoid a leak
      // ... sometime later ...
      signal.removeEventListener('abort', onAbort);
    }
    
    // Usage
    const ac = new AbortController();
    setTimeout(() => ac.abort(), 1000); // give it a 1s timeout
    try {
      await somethingIWantToCancel({ signal: ac.signal });
    } catch (e) {
      if (e.name === 'AbortError') {
        // deal with cancellation in caller, or ignore
      } else {
        throw e; // don't swallow errors :)
      }
    }
    

    No. We can't do that yet.

    ES6 promises do not support cancellation yet. It's on its way, and its design is something a lot of people worked really hard on. Sound cancellation semantics are hard to get right and this is work in progress. There are interesting debates on the "fetch" repo, on esdiscuss and on several other repos on GH but I'd just be patient if I were you.

    But, but, but.. cancellation is really important!

    It is, the reality of the matter is cancellation is really an important scenario in client-side programming. The cases you describe like aborting web requests are important and they're everywhere.

    So... the language screwed me!

    Yeah, sorry about that. Promises had to get in first before further things were specified - so they went in without some useful stuff like .finally and .cancel - it's on its way though, to the spec through the DOM. Cancellation is not an afterthought it's just a time constraint and a more iterative approach to API design.

    So what can I do?

    You have several alternatives:

    Using a third party library is pretty obvious. As for a token, you can make your method take a function in and then call it, as such:

    function getWithCancel(url, token) { // the token is for cancellation
       var xhr = new XMLHttpRequest;
       xhr.open("GET", url);
       return new Promise(function(resolve, reject) {
          xhr.onload = function() { resolve(xhr.responseText); });
          token.cancel = function() {  // SPECIFY CANCELLATION
              xhr.abort(); // abort request
              reject(new Error("Cancelled")); // reject the promise
          };
          xhr.onerror = reject;
       });
    };
    

    Which would let you do:

    var token = {};
    var promise = getWithCancel("/someUrl", token);
    
    // later we want to abort the promise:
    token.cancel();
    

    Your actual use case - last

    This isn't too hard with the token approach:

    function last(fn) {
        var lastToken = { cancel: function(){} }; // start with no op
        return function() {
            lastToken.cancel();
            var args = Array.prototype.slice.call(arguments);
            args.push(lastToken);
            return fn.apply(this, args);
        };
    }
    

    Which would let you do:

    var synced = last(getWithCancel);
    synced("/url1?q=a"); // this will get canceled 
    synced("/url1?q=ab"); // this will get canceled too
    synced("/url1?q=abc");  // this will get canceled too
    synced("/url1?q=abcd").then(function() {
        // only this will run
    });
    

    And no, libraries like Bacon and Rx don't "shine" here because they're observable libraries, they just have the same advantage user level promise libraries have by not being spec bound. I guess we'll wait to have and see in ES2016 when observables go native. They are nifty for typeahead though.