JavaScript/TypeScript Async Tips
Tags: javascript, typescript, productivity
Published: 2019-07-12 08:39:51

Please feel free to add more tips.

# Do not create async if only last return statement has await

```typescript

public async fetchList(): Promise<T> {

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

}

```

You can omit `async/await` here


```typescript

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...

```typescript

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.

```typescript

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

}
```

# Use `Promise.all`

```typescript

   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..


```typescript

   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, 


```typescript

   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

```typescript
   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,

```typescript
   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.

```typescript

   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

```typescript

   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.

```typescript

   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

```typescript
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

```typescript

   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.