Tuesday, September 17, 2019

Javascript callback, promise, await and observable

In traditional language, parallel calculation is handled by multithread. Basically, the application creates multiple threads and the operation system is responsible to manage and schedule time slice for each thread to run.

For javascript (and typescript), there is only one thread allow to run in a application, so multiple thread is not an option. Instead, async method is used to support parallel calculation, so that multiple tasks can run at the same time. Callback method, promise, async-await, and observor are some common ways for async method call. A simple way to distinguish sync and async method is for sync method, the next line after calling the sync method will not be called until the actual result is returned, while for async method, the next line will be called immediately no matter how long it will take to actually get the result.

1. callback is simplest way for executing async call, it passes the callback method to handle the result later, so there is no need to block the current call stack.
The drawback is when executing an async method, the onsuccess or onerror callback methods needs to be passed into the async method as parameters. If an async method needs to call another async method, then all the callback methods needs to be chained together, which causes the code not easy to read or maintain.

The below is a sample of using callback for async method to first build world, and then build a home.

 
  buildTheWorld(onsuccess, onerror) {
    // tslint:disable-next-line: only-arrow-functions
    setTimeout( () => {
      const r = Math.random();
      if (r > 0.5){
        console.log('build world successful');
        onsuccess(r);
      } else {
        console.log('build world failed');
        onerror();
      }
    }, 3000);
  }
  buildTheHome(r, onsuccess, onerror){
    // tslint:disable-next-line: only-arrow-functions
    setTimeout( function() {
      const s = Math.random();
      if (s > 0.5) {
        const room = r * 10;
        console.log('build home successful');
        onsuccess(room);
      } else {
        console.log('build home failed');
        onerror();
      }
    }, 3000);
  }
//not easy to read the caller code
 onClickMe() {
    const that = this;
    this.buildTheWorld(
      (r) => {
        that.buildTheHome(r, 
          // tslint:disable-next-line: only-arrow-functions
          (room) => {
            console.log('My new home has ' + room + ' rooms');
          },
          () => {
            console.log('Homeless again');
          }
        );
      },
      () => {
            console.log('No home without a world');
      }
    );
}
2. Promise can be used to simplify the javascript callback syntax, where, caller sets the success or error callback method on the returned promise object.

Theoretically, when implementing an async method, the logic should not need to care about the callback method information, such as how the result will be processed by caller, that information should be managed by caller, and should not pass into the async method. This is how promise handles the javascript async method call.

Basically, the async method will not accept onsuccess and onerror callback, instead, it returns an promise object to caller. Caller can set callback method to handle the promise result using Promise.then method. The creator of the promise calls resolve or reject to send the async result to the caller for consumption. Once the async result is available, the caller's callback method will be called. In this way, the callback method is limited in caller's scope and never need to be passed into async method as parameters. Then method will return immediately, but it will call the actual result handler until after the result is available in future.

Another benefit of using promise is chaining promise result. A promise handle can return a new promise, and so the then or catch method can be chained to sequentially handle the async request and result. Similar to try/catch, when an error or exception happens, all resolve handler will be skipped until it gets the first error/catch handler to handle the error.

The below sample is the function implemented using promise
  promiseTest() {
    this.buildTheWorldPromise()
    .then(result => {
        console.log('world is built');
        return this.buildTheHomePromise(result);
    })
    .then(r => {
        console.log('home build success with ' + r + ' rooms');
    })
    .catch((e) => {
        console.log(e); // get here if failed to build world or build home
    });
  }
  buildTheWorldPromise(): Promise<any> {
    const p = new Promise((resolve, reject) => {
      setTimeout( () => {
        const r = Math.random();
        if (r > 0.5) {
          console.log('build world successful');
          resolve(r);
        } else {
          console.log('build world failed');
          reject('world is falling');
        }
      }, 3000);
    });
    return p;
  }
  buildTheHomePromise(r): Promise<any> {
    const p = new Promise((resolve, reject) => {
      setTimeout( () => {
        const s = Math.random();
        if (s > 0.5) {
          const room = r * 10;
          console.log('build home successful');
          resolve(room);
        } else {
          console.log('build home failed');
          reject('home is falling');
        }
      }, 3000);
    });
    return p;
  }

3. Await can be used to simplify the code and write synchronous method with promise.
Basically, when a function definition includes async keyword, it tells this method will return a promise, instead of data type specified by the method body's return statement. For example, if an async method returns an integer, then the actual return type to the caller is a promise which wraps a integer data. When the actual data (like a string or integer) is resolved by the promise, that is, when the async method returns from the function, the actual return data will be resolved by promise, and then return to the caller in its then method. 
Within an async function, for any method call that returns a promise, or any async method, add await keyword before the call indicates the call in the current method will be blocked and wait until the promise is fulfilled with actual returned value, either resolved or rejected, before the next line of code gets executed to handle the returned data. As a result, the returned value after await is no longer the promise type returned by calling method, instead, await will get the actual data type fulfilled by the promise. This effectively allow developers to write synchronous code when calling asynchronous method. 
The above sample can be simplified as below using await method
  async awaitTest() {
    try {
     // returned r is actual data, not a promise
      const r = await this.buildTheWorldPromise();
      console.log('world is built: ' + r);
      const h = await this.buildTheHomePromise(r);
      console.log('home build success with ' + h + ' rooms');
    } catch (e) {
      console.log(e);
    }
  }
4. Observable
One issue with promise is it can only handle a single result. If an async operation returns multiple result to caller, observable can be used to handle the async result. 
Observable is implemented by RsJx library as a common pattern. 
Similar to promise, the creator of the observable can call observable.next to send success result to caller, or call observable.error to send failed result to caller, in addition, caller can also call observable.complete to tell caller the async operation is finished. 
Unlike promise, the observable creator can call observable.next multiple times to send multiple result to caller to process. Although it can only call observable.error or observable.complete once.