NodeJS 101 - Lazy Initialization
Here is a small problem that never looks hard until it bites you.
You build a class that needs a database connection. The constructor runs, but the connection is not ready yet because setup uses asynchronous initialization. Someone calls a method. It fails.
The core question is not how to connect. It is how to model the lifecycle so the caller knows when the resource is safe to use.
These are the two patterns I reach for. The post is about when each one fits, and why I prefer the first.
1. Explicit initialization
The simplest option is to make initialization part of the application programming interface (API).
The caller creates the object, then calls connect() before using it.
const redis = require('redis');
const crypto = require('crypto');
class DistributedDataStructure {
constructor() {
this.client = redis.createClient();
}
async connect() {
await this.client.connect();
}
async add(staffName, reviewId) {
const accountName = await this.client.get(staffName);
return this.client.sAdd(`v1:${accountName}:pending-reviews`, reviewId);
}
}
(async () => {
const ds = new DistributedDataStructure();
await ds.connect();
await ds.add('Jerome', crypto.randomBytes(12).toString('hex'));
})();
Why this works well
- easy to understand
- initialization is explicit
- failures happen in one place
- very little hidden behavior
Trade-off
The caller must remember to call connect().
In a small codebase, that may be fine. In a bigger or older one, someone will eventually forget.
How to spot the problem
If you see classes that throw connection errors on first method call, or tests that mysteriously fail unless run in a certain order, check whether initialization is explicit.
2. Proxy-based lazy initialization
Sometimes you need to create the object in synchronous code, but you still want the first real method call to wait for setup.
That is where the proxy pattern, implemented here with JavaScript's Proxy, can help.
The example below is a pattern sketch, not a canonical Redis recipe. The exact readiness signal depends on the client library.
const redis = require('redis');
const { once } = require('events');
const crypto = require('crypto');
class ProxiedDistributedDataStructure {
constructor() {
this.client = redis.createClient();
this.client.connect();
return new Proxy(this, {
get(target, property) {
const value = target[property];
if (typeof value !== 'function') {
return value;
}
return async function (...args) {
if (!target.client.isReady) {
await once(target.client, 'ready');
}
return value.apply(target, args);
};
},
});
}
async add(staffName, reviewId) {
const accountName = await this.client.get(staffName);
return this.client.sAdd(`v1:${accountName}:pending-reviews`, reviewId);
}
}
(async () => {
const ds = new ProxiedDistributedDataStructure();
await ds.add('Jerome', crypto.randomBytes(12).toString('hex'));
})();
When this helps
Use this when:
- the object must be created in synchronous code
- changing all callers is too expensive
- you are working with legacy initialization flows
- you want the first method call to absorb setup cost
Trade-offs
This is nicer for callers, but harder to reason about.
A method call that looks immediate may now wait on hidden setup work.
That means:
- debugging gets harder
- errors can show up later
- method wrapping needs care
- the abstraction is easier to break
A common mistake
If the initialization fails, the proxy will likely retry on every method call. Consider caching the promise itself and handling rejection explicitly, or adding a failed-state guard.
So while the Proxy approach is useful, I still treat it as the riskier tool.
The decision framework
The broader point is that async resource initialization is part of your API design, and the choice between patterns depends on who controls the caller.
A simple heuristic:
- use
connect()when you own the callers and want a clear lifecycle - use a
Proxywhen you cannot change the callers and compatibility matters more - use a factory function—a function that creates and returns a ready object—when you can afford to make construction async
Final note
Some client libraries already provide buffering or connection management that reduces the need for custom lazy wrappers.
Either way, make the initialization contract explicit, or hide it very carefully.
Quick checklist
Before adding lazy initialization, ask:
- Can I make the constructor caller async instead?
- Will the first-method-call latency surprise anyone?
- What happens if initialization fails?
- Can I test this without mocking the Proxy behavior?
