How to do asynchronous operation in object constructor

NAVIGATION

Contract for object constructor prevents returning Promise

Approach 1 - Start call in constructor, await completion in method

Approach 2 - Use async initializer() method

Approach 3 - Use asynchronous factory function

Objects are a good way to encapsulate data. You provide an interface to the consumer without revealing the internal state and implementation details. Sometimes you'd like to make an asynchronous call while initializing the object, but it's not that straightforward to make it happen.

How do you make an asynchronous call in object constructor?

As an example, we'll create a class that represents a web page. It accepts a url in the constructor and it provides two methods: getting the contents of the web page and calculating a hash of the contents.

class WebPage {
  constructor(url) {
    this.url = url;
  }

  getContents() {}
  calculateHash() {}
}

Contract for object constructor prevents returning Promise

How a constructor works is, when you call new WebPage("url") the constructor is executed with a fresh new object {} is set as this. The constructor modifies the object setting and initializing variables. Finally, as the constructor returns, that object becomes the result of the new WebPage("url") expression.

Since the new-expression returns the newly created object, returning a Promise is not an option. Therefore it's not possible to perform an asynchronous call to its completion during this process.

There are few approaches to work around this.

Approach 1 - Start call in constructor, await completion in method

Since performing an asynchronous call to its completion in the constructor is not an option, we can still start a call in the constructor. We'd start it in the constructor, save the unsettled Promise in an instance variable, and then await for its completion in the methods that need it.

class WebPage {
  constructor(url) {
    this.url = url;
    // start call
    this.responsePromise = axios(url);
  }

  async getContents() {
    // await for completion
    const response = await this.responsePromise;
    return response.data;
  }

  async calculateHash() {
    // await for completion
    const response = await this.responsePromise;
    return md5(response.data);
  }
}

We'll wait for the Promise's completion in all the places we need the result. The first method to be executed will do the actual waiting, and any subsequent calls will use the already resolved value.

Important: Need to call method in same tick

Theres one catch. If the call throws an exception, you'll get an unhandled promise rejection unless you call one of the methods during the same tick.

// this would work
try {
  const page = new WebPage("https://invalid.url");
  console.log(await page.calculateHash());
} catch (e) {
  // handle exception
}
// this would not work
// await only in later tick, rejection happens in current tick
try {
  const page = new WebPage("https://invalid.url");
  await somethingelse(); // this causes the problem
  console.log(await page.calculateHash());
} catch (e) {
  // handle exception
}

Approach 2 - Use async initializer() method

Another approach is to dedicate the constructor for initializing variables and have a separate asynchronous method that performs the asynchronous calls. Then, your constructor would not start any calls, only the initializer method would.

The user could call the initializer method manually.

class WebPage {
  constructor(url) {
    this.url = url;
  }

  async initialize() {
    this.response = await axios(this.url);
  }

  getContents() {
    return this.response.data;
  }

  calculateHash() {
    return md5(this.response.data);
  }
}

const page = new WebPage("https://google.com");
await page.initialize();
console.log(page.calculateHash());

Or, the initializer method could be internal and act as a guard at the beginning of each method. In this approach, we'd also have cover the possibility of concurrent calls causing the initializer to be called multiple times. The initializer methods are prefixed with underscore (_) to demarkate they're not part of the public interface.

class WebPage {
  constructor(url) {
    this.url = url;
  }

  async _doInitialize() {
    this.response = await axios(this.url);
  }

  async _initialize() {
    // prevent concurrent calls firing initialization more than once
    if (!this.initializationPromise) {
      this.initializationPromise = this._doInitialize();
    }
    return this.initializationPromise;
  }

  async getContents() {
    await this._initialize();
    return this.response.data;
  }

  async calculateHash() {
    await this._initialize();
    return md5(this.response.data);
  }
}

const page = new WebPage("https://google.com");
console.log(await page.calculateHash());

Approach 3 - Use asynchronous factory function

This is similar to using initializer method, but we're not exposing the constructor ever to be called on its own. Instead, we'd provide an asynchronous function that both instantiates the object and calls the initializer. This could be called a factory function.

class WebPage {
  /* ... */
}

// asynchronous factory function
async function createWebPage(url) {
  const webPage = new WebPage(url);
  await webPage.initialize();
  return webPage;
}

module.exports = {
  createWebPage
};

In this case it's helpful if we can hide the visibility of the constructor and prevent it from ever being called directly. It'd be only called through the factory method.

Node doesn't wait for your database call to finish?

Node doesn't wait for your database call to finish?

Learn how asynchronous calls work and make your app run as you intended. Get short email course on asynchronicity and two chapters from Finish Your Node App.

Loading Comments