109

I have a Promise. I created it to cancel an AJAX request if needed. But since I don't need to cancel that AJAX, I've never resolved it and AJAX completed successfully.

A simplified snippet:

var defer = $q.defer();
$http({url: 'example.com/some/api', timeout: defer.promise}).success(function(data) {
    // do something
});

// Never defer.resolve() because I don't need to cancel that ajax. What happens to this promise after request?

Do never resolved promises like that cause memory leaks? Do you have any advice about how to manage Promise life cycle?

Alexander Abakumov
  • 13,617
  • 16
  • 88
  • 129
Umut Benzer
  • 3,476
  • 4
  • 36
  • 53
  • 5
    A "never resolved" promise can still be "rejected". The word you were looking for was "unfulfilled". – Steven Vachon Sep 19 '18 at 12:32
  • $http is an interesting example because eventually an HTTP request will timeout (or otherwise yield an error response), if the client cannot reach the server, regardless of the promise passed to the 'timeout' argument. – ryanwebjackson May 13 '20 at 14:03

1 Answers1

157

Well, I'm assuming you don't keep an explicit reference to it since that would force it to stay allocated.

The simplest test I could think of is actually allocating a lot of promises and not resolving them:

var $q = angular.injector(["ng"]).get("$q");
setInterval(function () {
    for (var i = 0; i < 100; i++) {
        var $d = $q.defer();
        $d.promise;
    }
}, 10);

And then watching the heap itself. As we can see in the Chrome profiling tools, this accumulates the needed memory to allocate a 100 promises and then just "stays there" at less than 15 megabyes for the whole JSFIddle page

enter image description here

From the other side, if we look at the $q source code

We can see that there is no reference from a global point to any particular promise but only from a promise to its callbacks. The code is very readable and clear. Let's see what if you do however have a reference from the callback to the promise.

var $q = angular.injector(["ng"]).get("$q");
console.log($q);
setInterval(function () {
    for (var i = 0; i < 10; i++) {
        var $d = $q.defer();
        (function ($d) { // loop closure thing
            $d.promise.then(function () {
                console.log($d);
            });
        })($d);
    }
}, 10);

enter image description here

So after the initial allocation - it seems like it's able to handle that as well :)

We can also see some interesting patterns of GC if we let his last example run for a few more minutes. We can see that it takes a while - but it's able to clean the callbacks.

enter image description here

In short - at least in modern browsers - you don't have to worry about unresolved promises as long as you don't have external references to them

Benjamin Gruenbaum
  • 270,886
  • 87
  • 504
  • 504
  • 8
    Wouldn't this mean that if a promise takes too long to resolve (but would *eventually* resolve), it's at risk of being GC'd? – w.brian Nov 13 '14 at 15:06
  • @w.brian I wrote - "I'm assuming you don't keep an explicit reference to it since that would force it to stay allocated.", do you think I should clarify on that? (p.s. you're not at risk) – Benjamin Gruenbaum Nov 13 '14 at 15:16
  • Well if the promise is invoked within a function for example, any explicit reference to the promise would go out of scope immediately, making it a candidate for GC if your answer is correct (which I'm not doubting, just trying to make sure I understand the implications of an unresolved to "takes too long to resolve" promise). – w.brian Nov 13 '14 at 15:31
  • 8
    @w.brian unless you assign it somewhere - for example to a variable: `var b = $http.get(...)` or add a callback to it. That's also having a reference to it. If something resolves it (like you said - too long to resolve still means resolve) - it has to have a reference to it. So yes - it will not be GC'd – Benjamin Gruenbaum Nov 13 '14 at 15:33
  • 3
    Gotcha, that's what I thought. So, the question is "Do never resolved promises cause memory leak?" The answer, for the common use-case where a callback is passed to the promise, is yes. This line in your answer seems to contradict that: "We can also see some interesting patterns of GC if we let his last example run for a few more minutes. We can see that it takes a while - but it's able to clean the callbacks." Sorry if I'm being pedantic and nit-picky, I'm just trying to make sure I understand this. – w.brian Nov 13 '14 at 16:19
  • @w.brian and if the callback these promises had have registered an event listener on the root scope for instance - there would be a reference to them forever. Note that none of the examples actually effects external state and the GC is able to figure that out. – Benjamin Gruenbaum Nov 13 '14 at 16:39
  • 1
    That doesn't seem to make sense to me. If I had created 100.000 promises that console.log()'ed some line. I would like those 100.000 to log those lines if they suddenly resolve by some magic. Or are you saying that the browser will know that this will *never* resolve, since neither *I* nor the actual browser has any reference to it (nothing impacts it) - so how could that ever be true? (hmm, I can see that could be true) – odinho - Velmont Jan 13 '16 at 12:52
  • 12
    There's some truth in these comments and some that's misleading, so let me clarify: A promise with handlers attached *might* be eligible for garbage collection. A promise is kept alive (not GC-eligible) if *any* of the following are true: (1) there is a reference to the promise object, (2) there is a reference to the "deferred" state of the promise (the object/functions you use to resolve/reject it). Outside of this, the promise is eligible for GC. (If nobody has the promise and nobody can change its state, what is its purpose anymore, anyway?) – cdhowie Aug 29 '16 at 18:40
  • @cdhowie, that's talking about a promise library, right? Native promises will get GCd if there is no reference to the reject/resolve functions? – Mihail Malostanidis Mar 23 '18 at 00:21
  • @MihailMalostanidis It's talking about all promises. Even if the resolve/reject function objects are GC-eligible, we can't GC the promise if someone is holding a reference to the promise object. I can't think of a way that native implementations and promise libraries could differ on GC eligibility of promises. – cdhowie Mar 23 '18 at 00:41
  • @cdhowie If the scope of the function inside the promise constructor finished, we can GC that scope. Once that scope is collected, we can also assume no callbacks will ever be called, so references to all success and rejection handlers can also be dropped. And so, also any suspended functions that are awaiting the promise, etc... All while there's still a reference to the Promise object from outside. New handlers can even be added, but they should all die. – Mihail Malostanidis Mar 23 '18 at 01:06
  • @MihailMalostanidis We can't assume anything of the sort. The resolve/rejection function objects could be referenced from elsewhere: https://pastebin.com/3YSPgrrX The collection of the promise constructor function scope isn't useful as any sort of GC clue. I stand by my comment from 2016-07-29. – cdhowie Mar 23 '18 at 01:12
  • @cdhowie I meant once they're not referenced. eg `global.promiseState = undefined` – Mihail Malostanidis Mar 25 '18 at 15:21
  • 1
    An "unresolved" promise can be "rejected". The word you were looking for was "unfulfilled". – Steven Vachon Sep 19 '18 at 12:30
  • Thanks for that - actually unresolved can also mean “not following another promise”. Unresolved doesn’t just mean settled - the terminology is quite confusing at times :) – Benjamin Gruenbaum Sep 21 '18 at 17:51
  • @cdhowie How about if the constructor of a Promise contains an XHR request which is long lived? There is no outside reference to the Promise or the XHR. But the XHR holds a reference to the resolve/reject functions and I believe the XHR won't be GCed as long as the connection is active even though there are no references to it. Would this prevent the Promise and any scopes it holds from being GCed? – Kernel James Feb 01 '22 at 05:15
  • @KernelJames an http connection is an external reference to the promise. – Benjamin Gruenbaum Feb 01 '22 at 09:11
  • @KernelJames An active XHR request is kept alive internally by the browser. Yes, the completion handler of the XHR would be kept alive and therefore the promise is still referenced and ineligible for GC. – cdhowie Feb 01 '22 at 20:02