I've touched on this topic a bit in the past, adding an .abort() method to the promise returned by $http in AngularJS. But, in that exploration, the calling context calls .abort() directly on the promise, which makes the promise itself seem "cancelable."
I've been thinking about how to reconcile this pragmatic need with what Kyle is saying. And, it occurs to me that perhaps I just need to invert the dependencies. Rather than having a promise rely on a "cancel" or "abort" method, what if the cancelation happens at a higher level; what if the cancelation happens at the "factory" level (ie, the data services tier) and depends on the promise?
$timeout.cancel( timer );
In this way, the timer itself isn't altered - the control flow is. Now, what if we think about this generically:
PromiseFactory.cancel( promise );
In this way, we're not canceling the promise itself - we're altering something related to the promise workflow. This allows the calling context to be decoupled from the cancelation implementation and even allows the calling context to pass-in invalid or already-resolved promises.
Let's take a quick look at this in the context of making AJAX requests in AngularJS. In the following demo, I have a friendService that encapsulates AJAX requests and returns promises. The friendService exposes a .cancel() method that will take a promise and abort the underlying AJAX request. In doing so, it doesn't actually cancel the promise - it cancels underlying asynchronous request.
As you can see, I still need to store a reference to the AJAX-abort timer; but, I'm doing so in such a way that the calling context doesn't need to know about it. Now, that's not to say that the calling context is completely decoupled from the implementation - it still needs to know about the .cancel() method and that the cancel method will only work with the original promise. But, some level of coupling is necessary as the promise is, ultimately, coupled to the underlying AJAX request that needs to be canceled.
Now keep in mind, I'm not "aborting" the promise; meaning, I'm not preventing the promise from completing. I'm simply canceling part of the workflow. Calling .cancel() won't kill the promise - it will reject the step in the workflow associated with the promise. This allows all the touch-points for said promise to remain in tact and to function as expected. The only caveat being that contexts that handle the "reject" event may need to further branch their control flow to handle rejection "reasons" caused by cancelations.
Want to use code from this post? Check out the license.