DEV Community

Akash Kava
Akash Kava

Posted on

JavaScript/TypeScript Async Tips

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(...);

}

Enter fullscreen mode Exit fullscreen mode

You can omit async/await here


public fetchList(): Promise<T> {

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

}

Enter fullscreen mode Exit fullscreen mode

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);
   }

}

Enter fullscreen mode Exit fullscreen mode

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;
   }

}
Enter fullscreen mode Exit fullscreen mode

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;
   }
Enter fullscreen mode Exit fullscreen mode

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);
   }
Enter fullscreen mode Exit fullscreen mode

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);
   }
Enter fullscreen mode Exit fullscreen mode

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();
   }
Enter fullscreen mode Exit fullscreen mode

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()]);
   }
Enter fullscreen mode Exit fullscreen mode

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;
   }

Enter fullscreen mode Exit fullscreen mode

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);

Enter fullscreen mode Exit fullscreen mode

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();
   }

Enter fullscreen mode Exit fullscreen mode

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);
    }

}
Enter fullscreen mode Exit fullscreen mode

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;

   }

Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)