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.
Top comments (0)