Reusing Promises in JavaScript.

December 1, 2021

3 min read

Lately, we started a project to improve the performance of our main app. We identified a few API calls we were calling a lot. Results of these calls can change but not very very often so it's not an issue to cache the result for a minute or so.

So I implemented a very easy cache that will reuse active promises and returns already resolved results for a minute after an initial resolution.

This article will go over the code in detail. Let's start by simulating a parametrized API call.

function getData(key){
    return new Promise(function(resolve, reject) {
    console.log('starting get ' + key)

    setTimeout(() => {
        console.log('ending get ' + key)
        resolve(key);
    }, 1000);
  })
}

Easy enough.

Now we need a few variables where we store our promises, results, and resolution times. We will also create a new function that we will be calling to get the cached results.

const _cacheValues = new Map();
const _cacheResolvedTime = new Map();
const _cachePromises = new Map();

const getDataCached = function (key) {
}

The _cacheValues will hold already resolved values, _cachePromises will hold Promises in progress and _cacheResolvedTime will hold a time when the promise for the key was resolved last.

Now we will add a simple if statement that will be the basic stone of our cache.

if (_cacheValues.has(key)) {
  return Promise.resolve(_cacheValues.get(key));
} else if (_cachePromises.has(key)) {
        return _cachePromises.get(key);
} else {
  const promise = new Promise(function (resolve, reject) {
    return getData(key).then(data => {
      _cacheValues.set(key, data);
      _cachePromises.delete(key);
      const now = new Date().getTime();
      _cacheResolvedTime.set(key, now);
      resolve(data);
    });
  });
  _cachePromises.set(key, promise);
  return promise;
}

If we already have a value for a key let's return that. If we have a Promise in progress return that. If we have no data for that key, we will trigger the original method. This trigger will be wrapping its promise so that we fill our cache on resolve.

Now we will add the time to the live feature. At the start of our new method, we will add.

const now = new Date().getTime();

if (_cacheResolvedTime.has(key)) {
  if ((now - _cacheResolvedTime.get(key)) > 60000) {
  _cacheResolvedTime.delete(param);
  _cacheValues.delete(key);
  _cachePromises.delete(key);
  }
}

If we have it resolved and the resolution time is more than 60 seconds, we will remove it from our caches and continues to witch the rest of our logic.

Now we are done, we can test our code.

getDataCached('a').then(result => { console.log('first call outer: ' + result);
    getDataCached('a').then(result => { console.log('first call inner: ' + result); });
});

getDataCached('b').then(result => { console.log('first call outer: ' + result);
    getDataCached('b').then(result => { console.log('first call inner: ' + result); });
});

getDataCached('a').then(result => { console.log('second call outer: ' + result);
    getDataCached('a').then(result => { console.log('second call inner: ' + result); });
});

setTimeout(() => {
	getDataCached('a').then(result => { console.log('later call outer: ' + result);
    getDataCached('a').then(result => { console.log('later call inner: ' + result); });
});
}, 70000);

You can see the console result and this whole code in this Fiddle.


If you like this article you can follow me on Twitter.

← Back to blog