9

I chain a series of promises:

this.getData.then(this.getMoreData).then(this.getEvenMoreData);

At some point the user may decide to cancel the request and request something else.

How can I cancel the propagation of the chain?

Paul Nikonowicz
  • 3,883
  • 21
  • 39
panthro
  • 22,779
  • 66
  • 183
  • 324
  • 1
    What do you mean "the user"? This is a piece of code that's executed as quickly as possible, how would you add user intervention here? – tadman Jun 22 '15 at 19:12
  • The site is getting alot of data. If the user then requests other data, I want to halt getting the current set of data and outputting it. – panthro Jun 22 '15 at 19:12
  • So how would that play out? Is there a state object for this request that will indicate it was cancelled, like `var state = { running: true }` and then you can test `if (state.running) { return this.getMoreData() }` as necessary. – tadman Jun 22 '15 at 19:13
  • If you get your data via ajax you can cancel the request by [using the abort Method on the XMLHttpRequest object](http://stackoverflow.com/questions/446594/abort-ajax-requests-using-jquery). – Nick Russler Jun 22 '15 at 19:13
  • in case you use bluebird promises have a look at: https://github.com/petkaantonov/bluebird/blob/master/API.md#cancellation . Basically the cancellation just works my throwing a custom object you can then catch – Safari Jun 22 '15 at 19:14
  • Perhaps you shouldn't use a single promise for this. Have you considered writing an async loop? – Sacho Jun 22 '15 at 19:20
  • not clear what your asking. do you want to throw an exception or just cancel the propagation of the chain? – Paul Nikonowicz Jun 22 '15 at 19:23
  • cancel the propagation of the chain please – panthro Jun 22 '15 at 19:24

3 Answers3

8

You'd have to check for the state (of whether you should cancel or not) inside each chained method:

var userRequestedCancel = false;

this
   .getData()
   .then(function() {
     if(userRequestedCancel) {
      return Promise.reject('user cancelled');
     }

     return getMoreData();
   })
   .then(function() {
     if(userRequestedCancel) {
      return Promise.reject('user cancelled');
     }

     return getEvenMoreData();
   })

Or perhaps a slightly more elegant way (edited to pass context and arguments to callback methods)

var currentReq = false;
var userRequestedCancel = false;
var shouldContinue = function(cb,args) {
    if(userRequestedCancel) {
        return Promise.reject('user cancelled');
    }

    currentReq = cb.apply(this,args);
    return currentReq;
}

var onCancel = function() {
    userRequestedCancel = true;
    currentReq && currentReq.abort();
}

this
   .getData()
   .then(function() {
    return shouldContinue(getMoreData,arguments);
   })
   .then(function() {
     return shouldContinue(getEvenMoreData,arguments);
   })

If you need to cancel the current request as well, that is kind of trivial, set your current ajax request to be a global variable, and whatever event sets the userRequestedCancel flag to true, have that also cancel the ajax request (see edited code above)

Adam Jenkins
  • 51,445
  • 11
  • 72
  • 100
  • 1
    Asuming getData() is some ajax request, the request should be aborted when the answer wasn't received yet. – Nick Russler Jun 22 '15 at 19:18
  • @NickRussler - not really any more conceptually difficult. See edited code - keep a reference to the current ajax request and whenever you set `userRequestedCancel` to `true` - also cancel the last request. – Adam Jenkins Jun 22 '15 at 19:25
  • 1
    yup, just mentioned it because op said "the site is getting **alot of data** [...]" – Nick Russler Jun 22 '15 at 19:26
  • Thanks - what does the double && mean here? currentReq && currentReq.abort(); – panthro Jun 22 '15 at 20:36
  • @panthro - it's a shorthand if - if the statement on the left (i.e. `currentReq`) is truth-y, then evaluate what comes after the `&&` - It's the same as saying `if(currentReq) { currentReq.abort(); } ` https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Logical_Operators – Adam Jenkins Jun 22 '15 at 22:18
  • Just adding a note: good practice to let the Promise creator define how to reject a promise, and provide API for rejecting that API from other code as needed. Then if you use `async/await`, all `await` statements throw, going up the call stack. Usually you can leave any code in the stack untouched (unless you need to modify an existing try-catch block), and all code after the await statements are canceled! – trusktr Feb 08 '19 at 01:20
  • Then, at the top of the stack (wherever you wish the error propagation to stop) add a try catch to silence the cancellation error. This is great at cancelling a big call stack that is awaiting on a network request. Just make sure that any code that needs to clean up will clean up (i.e. add a try-catch where needed, and remember to pass the error along by re-throwing it). – trusktr Feb 08 '19 at 01:20
3

In order to cancel a promise chain you need to throw an error. Just have a look at the code below

function CancelError() {
    this.message = 'Cancelled';
}


obj
    .then(function() {
        throw new CancelError();
    })
    .catch(function(err) {
        if (err instanceof CancelError) {
            // Promise got cancelled
        }
        throw err; // throw the other mistakes
    });
Safari
  • 3,302
  • 9
  • 45
  • 64
  • 2
    How would I throw an error from outside of the chain? If the user starts a new chain, I need to cancel the current one. – panthro Jun 22 '15 at 19:21
0

Fun little challenge!

Without knowing exactly which task, request or process you're launching AND how the user can interrupt the said process, it's difficult to recommend any solution to "break" a .then(...) chain without doing some hack / trick that will trigger the .catch(...) rejection callback.

That being said, see if this example sheds some light at all.

Pay particular attention to the makeInterruptablePromise function and how it's used:

var bar = $('.progress-bar');
var h3 = $("h3");
var isEscape;

function log(msg, replace) {
  h3[replace ? 'html' : 'append'](msg + "<br/>");
}

$(document).keydown(e => {
  switch(e.keyCode) {
    case 27: //ESCAPE
      return isEscape = true;
    case 32: //SPACE
      return runDemo();
  }
});

function makeInterruptablePromise(cbStatus) {
  return new Promise((resolve, reject) => {
    function loop() {
      switch(cbStatus()) {
        case 1: return resolve();
        case -1: return reject();
        default: requestAnimationFrame(loop);
      }
    }
    //Don't forget to start the loop!
    loop();
  })
}


function runDemo() {
  log("Wait for it... (ESC to interrupt, SPACE to replay)", true);
  
  isEscape = false;
  
  var timeToComplete = 2000;
  var timeStart = Date.now();
  
  function updateBar() {
    var timeDiff = Date.now() - timeStart;
    var timePercent = timeDiff / timeToComplete;
    TweenMax.set(bar, {scaleX: 1 - timePercent});
    return timePercent > 1;
  }
  
  makeInterruptablePromise(() => {
    if(isEscape) return -1;
    if(updateBar()) return 1;

    return 0;
  })
    .then(() => log("Inside *then* chain."))
    .catch(() => log("Skipped *then* chain!"))
}

runDemo(); //Run first time.
body {
  background-color: #123456;
  color: #fff;
}

.progress-bar {
  display: block;
  width: 200px;
  height: 10px;
  background-color: #88f;
  transform-origin: top left;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/1.20.2/TweenMax.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

<div class="progress-bar"></div>

<h3></h3>

What this essentially boils down to, is I'm passing a callback to makeInterruptablePromise to "monitor" for 3 possible statuses.

  • If it's 1: It resolves (goes into your 'then')
  • If it's -1: It rejects (skips to your 'catch')
  • Otherwise, it just keeps looping by using the browser's requestAnimationFrame(...) method.
    (essentially a setTimeout(...) calibrated to trigger per screen refreshes).

Now, to affect how these statuses changes over time, I demonstrated this by using ESCAPE as the interrupt status (-1) and a timer that runs for 2 seconds. Once complete, the timer returns status (1).

Not sure it can fit your need, but could be useful for anyone else trying to break Promises via some external / asynchronous factor.

chamberlainpi
  • 4,854
  • 8
  • 32
  • 63