loading...

JavaScript/TypeScript Async Tips

akashkava profile image Akash Kava ・3 min read

Please feel free to add more tips.

Do not create async if only last return statement has await


public async fetchList(): Promise<T> {

   return await this.someService.fetchList(...);

}

You can omit async/await here


public fetchList(): Promise<T> {

   return this.someService.fetchList(...);

}

Both are logically same, unless compiler tries to optimize this automatically, you can simply avoid async/await.

Do not omit async/await when catching exceptions...

In above example, if you want to catch an exception... following code is wrong...


public fetchList(): Promise<T> {
   try {
      return this.someService.fetchList(...);
   } catch(e) {
       // report an error
      this.reportError(e);
      return Promise.resolve(null);
   }

}

This will never catch a network related error, following is the correct way.


public async fetchList(): Promise<T> {
   try {
      return await this.someService.fetchList(...);
   } catch(e) {
       // report an error
      this.reportError(e);
      return null;
   }

}

Use Promise.all


   public async fetchDetails(list: IModel): Promise<IDetail[]> {
       const details = [];
       for(const iterator of list) {
           const result = await this.someService.fetchDetails(iterator.id);
           details.push(result);
       }
       return details;
   }

There is sequential operation and it will take long time, instead try this..


   public fetchDetails(list: IModel): Promise<IDetail[]> {
       const details = list.map((item) => 
          this.someService.fetchDetails(item.id));
       return Promise.all(details);
   }

If you want to return null if any one fails,


   public fetchDetails(list: IModel): Promise<IDetail[]> {
       const details = list.map(async (item) => {
           try {
              return await this.someService.fetchDetails(item.id); 
           } catch (e) {
               this.reportError(e);
               return null;
           }
       });
       return Promise.all(details);
   }

You can use Promise.all without an array as well

   public async fetchDetails(): Promise<void> {
       this.userModel = await this.userService.fetchUser();
       this.billingModel = await this.billingService.fetchBilling();
       this.notifications = await this.notificationService.fetchRecent();
   }

You can rewrite this as,

   public fetchDetails(): Promise<void> {
       [this.userModel, 
          this.billingModel,
          this.notifications] = Promise.all(
              [this.userService.fetchUser(),
              this.billingService.fetchBilling(),
              this.notificationService.fetchRecent()]);
   }

Atomic Cached Promises

You can keep reference of previous promises as long as you wish to cache them and result of promise will be available for all future calls without invoking actual remote calls.


   private cache: { [key: number]: [number, Promise<IDetail>] } = {};

   public fetchDetails(id: number): Promise<IDetail> {
      let [timeout, result] = this.cache[id];
      const time = (new Date()).getTime();
      if (timeout !== undefined && timeout < time {
         timeout = undefined; 
      }
      if (timeout === undefined) {
         // cache result for 60 seconds
         timeout = time + 60 * 1000;
         result = this.someService.fetchDetails(id);
         this.cache[id] = [timeout, result];
      }
      return result;
   }

This call is atomic, so for any given id, only one call will be made to remote server within 60 seconds.

Delay


   public static delay(seconds: number): Promise<void> {
       return new Promise((r,c) => {
           setTimeout(r, seconds * 1000);
       });
   }


   // usage...

   await delay(0.5);

Combining Delay with Cancellation

If we want to provide interactive search when results are displayed as soon as someone types character but you want to fire search only when there is pause of 500ms, this is how it is done.


   public cancelToken: { cancelled: boolean } = null;   

   public fetchResults(search: string): Promise<IModel[]> {
       if (this.cancelToken) {
           this.cancelToken.cancelled = true;
       }
       const t = this.cancelToken = { cancelled: false };
       const fetch = async () => {
           await delay(0.5);
           if(t.cancelled) {
              throw new Error("cancelled");
           }
           const r = await this.someService.fetchResults(search);
           if(t.cancelled) {
              throw new Error("cancelled");
           }
           return r;
       };
       return fetch();
   }

This method will not call remote API if the method would be called within 500ms.

but there is a possibility of it being called after 500ms. In order to support rest API cancellation, little bit more work is required.

CancelToken class

export class CancelToken {

    private listeners: Array<() => void> = [];

    private mCancelled: boolean;
    get cancelled(): boolean {
        return this.mCancelled;
    }

    public cancel(): void {
        this.mCancelled = true;
        const existing = this.listeners.slice(0);
        this.listeners.length = 0;
        for (const fx of existing) {
            fx();
        }
    }

    public registerForCancel(f: () => void): void {
        if (this.mCancelled) {
            f();
            this.cancel();
            return;
        }
        this.listeners.push(f);
    }

}

Api Call needs some work... given an example with XmlHttpRequest


   public static delay(n: number, c: CancelToken): Promise<void> {
      return new Promise((resolve, reject) => {
         let timer = { id: null };
         timer.id = setTimeout(() => {
            timer.id = null;
            if(c.cancelled) {
                reject("cancelled");
                return;
            }
            resolve();
         }, n * 1000);
         c.registerForCancel(() => {
            if( timer.id) { 
               clearTimeout(timer.id);
               reject("cancelled");
            }
         });
      });
   }

   public async ajaxCall(options): Promise<IApiResult> {

      await delay(0.1, options.cancel);

      const xhr = new XMLHttpRequest();

      const result = await new Promise<IApiResult> ((resolve, reject)) => {

         if (options.cancel && options.cancel.cancelled) {
             reject("cancelled");
             return;
         }

         if (options.cancel) {
             options.cancel.registerForCancel(() => {
                xhr.abort();
                reject("cancelled");
                return;
             });
         }

         // make actual call with xhr...

      });

      if (options.cancel && options.cancel.cancelled) {
          throw new Error("cancelled");
      }
      return result;

   }

This can cancel requests that were cancelled by user as soon as he typed new character after 500ms.

This cancels existing calls, browser stops processing output and browser terminates connection, which if server is smart enough to understand, also cancels server side processing.

For best use, you can combine all tips and create best UX.

Posted on by:

Discussion

pic
Editor guide