scoped property
initialisation
programming
software development
coding concepts

awaiting the initialisation of a scoped property

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

A property cannot magically become awaitable just because its value arrives asynchronously. If a scoped property depends on network I/O, file I/O, or any other async setup, the usual fix is to await an initialization step, not the property itself.

In practice, that means exposing a promise-returning method, caching the initialization task, or providing a getter that returns a promise. The core idea is always the same: separate "this value exists" from "this object is ready to use."

Why Direct Property Access Does Not Work

Suppose a class needs to load configuration before one of its properties is valid. A first attempt often looks like this:

typescript
1class UserSession {
2  token!: string;
3
4  async load(): Promise<void> {
5    this.token = "abc123";
6  }
7}
8
9async function main() {
10  const session = new UserSession();
11  await session.load();
12  console.log(session.token);
13}
14
15main();

This works because the code awaits load(), which is the real asynchronous operation. The property token is just state. It has no built-in mechanism to pause execution until initialization finishes.

That distinction matters in every language with async support. Properties hold values; functions describe work.

Prefer an Explicit Initialization Method

The clearest pattern is to expose one method that must be awaited before dependent properties are used.

typescript
1class ConfigStore {
2  private initialized = false;
3  private apiUrl = "";
4
5  async initialize(): Promise<void> {
6    if (this.initialized) {
7      return;
8    }
9
10    await new Promise((resolve) => setTimeout(resolve, 50));
11    this.apiUrl = "https://api.example.com";
12    this.initialized = true;
13  }
14
15  getApiUrl(): string {
16    if (!this.initialized) {
17      throw new Error("ConfigStore has not been initialized.");
18    }
19
20    return this.apiUrl;
21  }
22}
23
24async function main() {
25  const store = new ConfigStore();
26  await store.initialize();
27  console.log(store.getApiUrl());
28}
29
30main();

This pattern is explicit and easy to test. Callers can see that there is a setup phase, and synchronous getters stay synchronous once initialization is complete.

Cache the Initialization Promise

If several callers may trigger setup, cache the promise so the initialization work runs only once:

typescript
1class ProfileService {
2  private profileName = "";
3  private initPromise: Promise<void> | null = null;
4
5  initialize(): Promise<void> {
6    if (this.initPromise) {
7      return this.initPromise;
8    }
9
10    this.initPromise = (async () => {
11      await new Promise((resolve) => setTimeout(resolve, 50));
12      this.profileName = "Ava";
13    })();
14
15    return this.initPromise;
16  }
17
18  async getProfileName(): Promise<string> {
19    await this.initialize();
20    return this.profileName;
21  }
22}
23
24async function main() {
25  const service = new ProfileService();
26  const [a, b] = await Promise.all([
27    service.getProfileName(),
28    service.getProfileName(),
29  ]);
30
31  console.log(a, b);
32}
33
34main();

This is often the best design for scoped services in web apps, because the first caller triggers initialization and later callers reuse the same in-flight work.

Returning a Promise From a Getter

Some codebases choose to expose a getter that returns a promise:

typescript
1class Settings {
2  private readonly readyValue: Promise<string>;
3
4  constructor() {
5    this.readyValue = (async () => {
6      await new Promise((resolve) => setTimeout(resolve, 50));
7      return "dark";
8    })();
9  }
10
11  get theme(): Promise<string> {
12    return this.readyValue;
13  }
14}
15
16async function main() {
17  const settings = new Settings();
18  console.log(await settings.theme);
19}
20
21main();

This is valid, but it is easy to misread. A property named theme usually looks synchronous, so a method such as getTheme() or initialize() is often clearer to other developers.

In other words, you can return a promise from a property, but that does not make the property itself special. It just means the property's value happens to be a promise object.

Design Around Readiness

When a scoped property depends on async work, the real design question is how the rest of the system learns that the object is ready. Good options include:

  • require an explicit initialize() call
  • expose async access methods that await setup internally
  • cache one initialization promise and reuse it

What you generally want to avoid is scattered code that reads a property and hopes some other part of the app initialized it first.

That kind of hidden dependency leads to race conditions that only appear under load or slow network conditions.

Common Pitfalls

  • Trying to await a plain property whose type is not a promise.
  • Storing async-dependent state in a property without exposing any readiness contract.
  • Running initialization multiple times because each caller starts its own async setup.
  • Hiding a promise behind an ordinary-looking property name that suggests synchronous access.
  • Reading a property before initialization finished and then debugging intermittent undefined or empty values.

Summary

  • Await the initialization work, not the property itself.
  • An explicit initialize() method is usually the clearest pattern.
  • Cache the initialization promise when multiple callers may race to set up the same scoped state.
  • A property can return a promise, but that is often less clear than an async method.
  • The real goal is to make object readiness explicit and predictable.

Course illustration
Course illustration

All Rights Reserved.