Search code examples
javascriptmemory-leaksbluebirdes6-promise

would sharing a global resolved promise between many promise chains create a memory leak or have performance downsides?


I often fall on a pattern of this kind:

function makeSureDataWasFetched () {
  if (dataAlreadyFetched) {
    // Creating a new empty resolved promise object and returning it
    // to keep the function interface consistent
    return Promise.resolve()
  } else {
    return fetchData()
  }
}

So to avoid re-creating a new empty resolved promise object every time, I have been playing with the idea of sharing a unique, global resolved promise

const resolved = Promise.resolve()

// then, re-use this resolved promise object all over the place
function makeSureDataWasFetched () {
  if (dataAlreadyFetched) {
    // Return the shared resolved promise object,
    // sparing the creation of a new resolved promise object
    return resolved
  } else {
    return fetchData()
  }
}

function makeSureSomethingElseWasFetched () {
  if (thatSomethingElseWasAlreadyFetched) {
    return resolved
  } else {
    return fetchSomethingElse()
  }
}

// etc

Being referenced all over the application, this resolved promise will never be garbage collected. Thus, if it keeps some reference to promise chains using it, those wouldn't be garbage collected either and that would create a memory leak, right?

So my question: would this kind of global resolved promise keep a reference to all those depending promise chains in Bluebird implementation? In vanilla ES6 Promises? If not, would that have any performance downside counter-balancing the spared cost of creating new resolved promises every time?


Solution

  • Would sharing a global resolved promise between many promise chains create a memory leak or have performance downsides?

    No.

    Reusing and sharing the globally resolved promise just keeps that single globally resolved promise from ever being garbage collected. It does not affect the garbage collection of other promises that may be chained to it. They will be garbage collected when they are no longer reachable just as normally happens.

    Now, it is unclear what advantage there is to sharing a globally resolved promise at all. It should not be needed. Anytime you want an already resolved promise, you can just create one with Promise.resolve() and then your code does not rely on a shared global and can be more modular.

    So my question: would this kind of global resolved promise keep a reference to all those depending promise chains in Bluebird implementation?

    No.

    In vanilla ES6 Promises?

    No.

    If not, would that have any performance downside counter-balancing the spared cost of creating new resolved promises every time?

    You are asking about a premature micro-optimization which is pretty much never a good idea. If at some point in the future, you wanted to totally optimize the performance of your code, then you would profile and measure and I can promise you that you'd find 100 things you could work on that would impact your code far more than trying to share a global for a resolved promise.


    To help you understand who has a reference to what in promise chaining and when things can be garbage collected, let me describe how chaining works. Let's say you have the following code:

    f1().then(f2).then(f3)
    

    f1 and f2 both return promises which we will call P1 and P2.

    So, here's the progression:

    1. Call f1() and it returns a promise P1.
    2. Call P1.then(f2) which returns a new promise we will call P3.
    3. Call P3.then(f3) which returns a new promise we will call 'P4'.
    4. Then, at some point in the future, P1 is resolved and triggers it to call its .then() handlers.
    5. f2 gets called and it returns promise P2.
    6. When the internals of the promise .then() code gets a return value from the f2 .then() handler as a return value, it detects that this return value is a promise and it then chains that new promise P2 to P3. It does that by adding it's own .then() handler to P2 so it can track its state. Internal to P3, it adds this new .then() handler to a list of things that have to happen before P3 can be resolved. Note that there's no direct reference between P2 and P3. They don't even have direct references to each other. Instead, P3 is waiting for a specific .then() handler (that happens to be attached to P2) to be called.
    7. Then, at some future time P2 resolves.
    8. This calls the internal .then() handler that was established in step 6 and any reference to that .then() handler is cleared. This tells P3 that it can now resolve itself and this unlinks its indirect reference to P2. At this point nothing in this promise chain has any reference to P2 any more so if there are no other references to it elsewhere in your code, it can be GCed.
    9. When P3 resolves, it calls its .then() handlers which will execute f3 telling your code that the promise chain is now done.

    So, from this I'm hoping you can see that chained promises don't actually store references to each other. The parent promise ends up with a .then() handler on the child promise which keeps the child promise from getting GCed until the .then() handler is called and once that .then() handler is called, even the indirect connection between the two promises is severed and each is independently available for GC (as long as there are no other references to them in other code).

    Per your question, if P2 happens to be your shared, global promise that is already resolved, then step 6 would just add a .then() handler to it which would be called on the next tick (since the underlying promise is already resolved) and once the .then() handler is called, there would be no connection at all to P2 any more, thus it doesn't matter that it's a persistent global or not.