Promise based semaphore pattern in Javascript

Promise based semaphore pattern in Javascript

Let's start with a singleton pattern when you want to instantiate at most one instance of a class, or lazily initiate a value.

let instance
const getInstance = () => instance === undefined
  ? instance = initiateInstance()//this is a heavy task and should be done at most once in the whole life of the application
  : instance

The problem arises when initiateInstance() returns a promise which fulfills the instance value.

Someone may think about changing the getInstance into an async function with another semaphore named running. Like

/* NOTE: THIS CODE SHOULD NOT BE USED */
/* THIS IS AN EXAMPLE OF BAD CODE */
let instance
let running
const getInstance = async () => {
  if (!instance) {
    if (running) return await initiateInstance()
    running = true
    try {
      instance = await initiateInstance()
    } finally {
      running = false
    }
  }
  return instance
}

If you enable eslint's require-atomic-updates rule, it will warn in the line running = false that

Possible race condition: `running` might be reassigned based on an outdated value of `running`              require-atomic-updates

This approach has 3 down-side effects

  • initiateInstance is executed more than once while it should be at most once
  • One more variable is required
  • eslint complains due to require-atomic-update rule

My suggestion, in this case, is as following

let instancePromise
const getInstance = () => instancePromise ||= (async () => {
  try {
    // await is a must. Otherwise, the error will not be caught
    return await initiateInstance()
  } catch (e) {
    instancePromise = undefined
    throw e
  }
})()

Simple coding, no eslint warning, and no more global variable required, and the most important point, the heavy initiateInstance function will never get called twice unless it failed in previous calls.

It is worth noting that if initiateInstance fails while execution, instancePromise needs to be reset. Otherwise, all following calls to getInstance() will be rejected.

Buy Me A Coffee