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 torequire-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.