Fun with Promises in JavaScript and TypeScript

If you’re writing asynchronous code – especially Javascript or Typescript – I have a story to share. It all started when I needed to initialize the Microsoft Teams JavaScript SDK for use in a web application I’m writing. It’s just a simple call in the browser:

await microsoftTeams.app.initialize();

This is all pretty easy, but the docs say you should only call initialize()  once and my app is a bit complex, with multiple web components that are rendered in parallel, and two of them need to use the Teams SDK on page load. So how can I prevent initialize() from being called more than once while isolating code within my web components?

Singleton promises

To prevent the multiple calls, I reached into my bag of geeky developer tricks and made this little function so it would only be called once:

let isInitialized = false;
export async function ensureTeamsSdkInitialized() {
    if (!isInitialized) {
        await microsoftTeams.app.initialize();
        isInitialized = true;
    }
}

And whenever I want to initialize, I do this:

await ensureTeamsSdkInitialized();

Pretty slick, right? I can call the function as often as I wish and it will only initialize the SDK the first time, right? Right???

Well, no not really. Maybe the problem is obvious to you, but at first I was sure this would work and was surprised when my code called initialize() twice, once for each of the two web components that used it. There’s a race condition here!


Self: “Say what, Javascript is single-threaded, how can there be a race condition?!”

Other self: “You’re still thinking like a synchronous programmer!”

Self: “Yeah, having multiple threads doing things in parallel made life so much easier, right? Except for locking and semaphores. And the extra memory to store a register set and stack per thread. And then you sometimes run out of threads, yeah that’s no fun. Well maybe it’s not ideal after all, but it seemed so easy at first! Anyway, I’m working on a web page and if I make synchronous calls it will freeze up while waiting for them, right?”

Other self: “Right. Now look closer. Race conditions don’t require threads, just concurrency.”


race condition is when the outcome of some operation is dependent on the timing of other unrelated or uncontrollable events. I have a personal motto: never bet on a race condition.

If one component has called ensureTeamsSdkInitialized()  and it’s waiting for the call to initialize() to complete when another component makes the same call, the isInitialized flag will still be false and we’ll call initialize() again.

In this scenario, I had two promises for each caller – four promises in all – two for the calls to ensureTeamsSdkInitialized() and two more for the initialize() calls! That’s a lot of promises for code that doesn’t work right!

The solution for this is to use only one promise and store it in a singleton like this:

let teamsInitPromise;
export function ensureTeamsSdkInitialized() {
    if (!teamsInitPromise) {
        teamsInitPromise = microsoftTeams.app.initialize();
    }
    return teamsInitPromise;
}

This works because microsoftTeams.app.initialize() returns a promise, and we don’t need to await it right away. Instead, we can save it and also return it for our caller to await. When the SDK resolves the promise, all of the callers will wake up and the initialize() call will have happened only once. Even when a caller comes along later, the same promise will immediately resolve and the caller won’t know or care that the initialize() was done long ago.

Notice that I no longer needed the async keyword on the ensureTeamsSdkInitialized() function because it’s not doing any awaits; it requests a promise and returns a promise with no syntactic sugar required.

Caching promises instead of data

Now suppose your application contains some service code that needs to fetch some data using a REST call, and this service is called from various places in the application. I ran into this exact situation in the same application where I was initializing the Teams SDK. I needed information about the user’s profile in two different web components, but didn’t want the overhead of doing the fetch twice.

I started with a simple caching mechanism where I stored the user profile data in a singleton, but I noticed I was still doing two fetches whenever the page rendered. This was because two web components requested the data when the page was loaded, before the data was received and cached. I considered some complex logic to keep track of this but realized it’s already built into the promise! By caching the promise instead of the data, I was able to fetch only once regardless of the timing.

let getUserProfilePromise;
export function getUserProfile() {
    if (!getUserProfilePromise) {
        getUserProfilePromise = getUserProfile2();
    }
    return getUserProfilePromise;
}

async function getUserProfile2() {
    // Fetch the data
    return userProfile;
}

// Anywhere we need the user profile, do this:
const userProfile = await getUserProfile();

Bonus content: Curt’s Caching Corollaries

All this reminds me of some really helpful caching advice I got from my friend Curt Devlin ages ago. This is my take on his advice, with a title I made up because I admire alliteration.

  1. Only cache data that changes less frequently than you’ll consume it. This might sound obvious but I’ve seen people cache things that are only used once! Overcome your urge to cache everything in sight and only use caching when you know there’s an efficiency to be gained. Along with that, make sure there’s a way to invalidate the cache when something changes, or that consumers can deal with slightly stale data.
  2. Always cache data as close to the consumer as possible. If there are multiple services or layers involved, generally you’re better off caching close to the code that needs the data and avoid extra calls to underlying services. For example, if your web service reads from a database, it would be better to cache the data in the web server than in the database server to avoid copying it between servers each time.
  3. Always cache data in its most processed form. For example, suppose you want to read something from a database and then format it as HTML. Why cache the raw data and then rebuild the HTML each time? You’ll waste cycles building the exact same output over and over; better to cache the data after it’s been processed.

Postscript

Come to think of it, if I had remembered Curt’s Caching Corollaries I might have gone directly to caching promises instead of trying to cache the data. Curt’s 3rd corollary applies here! Indeed, a promise for data is a more “processed” form than the data itself: it adds the ability to run code when the data becomes available. By not including this in my cache, I introduced false cache misses and failed to speed up the initial render of my web application.

Thanks for reading and if you like this article, feel free to cache it for future use!

Leave a comment