Singleton with async constructor in JavaScript
Singleton is really easy to write, but it’s not so simple, especially in multi-threading environments. Sure, JavaScript is single-threaded by design, but not all concurrency daemons are slayed.
Note: this is one of those posts leveraging Cunningham’s Law: the best way to get an answer is not to ask questions on the Internet, just post the wrong solution. To be honest I don’t know what’s the best way to handle Singleton with async constructor — I’m just trying to adjust best practices to JavaScript specific runtime.
Starting with the basics here’s Singleton implementation with standard blocking (non-async) constructor:
let instance;
const getInstance = () => {
if (!instance) {
instance = new Date();
}
return instance;
}
Note: all code examples assume to be confined within CommonJS modules (Node runtime), where only relevant parts are exported. I’ve skipped exports, classes, constructors and all the unnecessary cruft for clarity.
The code above is all fine when object creation actually happens in the same event loop tick as the instance variable check. Problems start to happen when it’s not the case. For example instance in question is a network connection that needs to be opened first. Such operations in Node are done as async call not to block main thread.
Async/Await
For asynchronous instance creation we can use following code:
const net = async () => await new Promise(resolve => setTimeout(resolve, 1000));let instance;
const getInstance = async () => {
if (!instance) {
instance = await net();
}
return instance;
}
Not much different than previous one — just an additional async/await pair. However, can you spot the problem already? At the “await net()” call there’s context switch, because promise is created, but it will be resolved (return value) only some time after it has been created. This is not happening all in one sequence on the main thread any more.
As a result we can have many calls of “net” instantiation potentially creating heavy objects in memory, where only one will be eventually used. Not only waste of CPU and RAM, but also using up the I/O resources. Amount of network sockets is limited! Not ideal, but eventually we’ll always have at least one usable instance to use.
JavaScript is single-threaded, but beware of context switches
The most reasonable solution is with simple async/await. It does not guarantee that constructor will be invoked only once, but at least once. Other requests will likely await Promise resolution — that’s the price to pay.
I’ve made an attempt to make sure only one call for constructor is invoked using Semapthore pattern, but there’s a risk of running into race condition when another request is made before await is done:
let instance;
let semaphore = false;
const getInstance = async () => {
if (!instance && !semaphore) {
semaphore = true; // mark awaited constructor
instance = await net();
}
return instance;
}
The code above will not work in all cases, because there’s a time window when semaphore===true but instance is not resolved yet. On other words: we risk that instance will be returned as undefined in some cases.
Bonus: blocking constructor in Node.js runtime
Bonus: in Node.js the heavy and blocking constructor can be invoked during server startup time. For that the code does not even have to be async, we can easily block the main thread loop. This way it can be initialized once and then instance can be exported via CommonJS module variable. That’s the simplest solution, but available only in Node.js runtime.
Node.js runtime is a bit more complicated than single-threaded JavaScript loop. I recommend watching the What the heck is the event loop anyway? video by Philip Roberts.