---
name: sync-construction-async-property-ui-render-gate-pattern
description: Sync construction with async property pattern. Use when creating clients that need async initialization but must be exportable from modules and usable synchronously in UI components.
metadata:
author: epicenter
version: '1.0'
---
# 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:
```typescript
// This doesn't work
export const client = await createClient(); // Top-level await breaks bundlers
```
So you end up with getter patterns:
```typescript
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:
```typescript
// 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:
```svelte
{#await client.whenSynced}
{: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:
```typescript
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` with `.whenSynced` |
## Real-World Example: y-indexeddb
The Yjs ecosystem uses this pattern everywhere:
```typescript
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.
## Related Patterns
- [Lazy Singleton](../lazy-singleton/SKILL.md) — when you need race-condition-safe lazy initialization
- [Don't Use Parallel Maps](../../docs/articles/instance-state-attachment-pattern.md) — attach state to instances instead of tracking separately
## References
- [Full article](/docs/articles/sync-construction-async-property-ui-render-gate-pattern.md) — detailed explanation with diagrams
- [Comprehensive guide](/docs/articles/sync-client-initialization.md) — 480-line deep dive