Sync Construction, Async Property
The initialization of the client is synchronous. The async work is stored as a property you can await, while passing the reference around.
When to Apply This Pattern
Use this when you have:
-
Async client initialization (IndexedDB, server connection, file system)
-
Module exports that need to be importable without await
-
UI components that want sync access to the client
-
SvelteKit apps where you want to gate rendering on readiness
Signals you're fighting async construction:
-
await getX() patterns everywhere
-
Top-level await complaints from bundlers
-
Getter functions wrapping singleton access
-
Components that can't import a client directly
The Problem
Async constructors can't be exported:
// This doesn't work export const client = await createClient(); // Top-level await breaks bundlers
So you end up with getter patterns:
let client: Client | null = null;
export async function getClient() { if (!client) { client = await createClient(); } return client; }
// Every consumer must await const client = await getClient();
Every call site needs await . You're passing promises around instead of objects.
The Pattern
Make construction synchronous. Attach async work to the object:
// client.ts export const client = createClient();
// Sync access works immediately client.save(data); client.load(id);
// Await the async work when you need to await client.whenSynced;
Construction returns immediately. The async initialization (loading from disk, connecting to servers) happens in the background and is tracked via whenSynced .
The UI Render Gate
In Svelte, await once at the root:
<!-- +layout.svelte --> <script> import { client } from '$lib/client'; </script>
{#await client.whenSynced} <LoadingSpinner /> {:then} {@render children?.()} {/await}
The gate guarantees: by the time any child component's script runs, the async work is complete. Children use sync access without checking readiness.
Implementation
The withCapabilities() fluent builder attaches async work to a sync-constructed object:
function createClient() { const state = initializeSyncState();
return {
save(data) {
/* sync method */
},
load(id) {
/* sync method */
},
withCapabilities({ persistence }) {
const whenSynced = persistence(state);
return Object.assign(this, { whenSynced });
},
};
}
// Usage export const client = createClient().withCapabilities({ persistence: (state) => loadFromIndexedDB(state), });
Before and After
Aspect Async Construction Sync + whenSynced
Module export Can't export directly Export the object
Consumer code await getX() everywhere Direct import, sync use
UI integration Awkward promise handling Single {#await} gate
Type signature Promise<X>
X with .whenSynced
Real-World Example: y-indexeddb
The Yjs ecosystem uses this pattern everywhere:
const provider = new IndexeddbPersistence('my-db', doc); // Constructor returns immediately
provider.on('update', handleUpdate); // Sync access works
await provider.whenSynced; // Wait when you need to
They never block construction. The async work is always deferred to a property you can await.
Alternate Pattern: Await in Every Method
Alternatively, you can skip the whenReady property entirely and hide the initialization await inside each method. The canonical example is idb:
const dbPromise = openDB('keyval-store', 1, { upgrade(db) { db.createObjectStore('keyval') } });
export async function get(key) { return (await dbPromise).get('keyval', key); } export async function set(key, val) { return (await dbPromise).put('keyval', val, key); }
Use whenReady when your client has sync methods that depend on initialized state. Use await-in-every-method when every method is async anyway (like database access). See the idb await-in-every-method article for a deeper comparison.
Related Patterns
-
Lazy Singleton — when you need race-condition-safe lazy initialization
-
Don't Use Parallel Maps — attach state to instances instead of tracking separately
References
-
Full article — detailed explanation with diagrams
-
Comprehensive guide — 480-line deep dive with idb example
-
idb await-in-every-method — the sibling pattern for purely async APIs