694 words
3 minutes
Avoid Async/Await Pitfalls in TypeScript

I was asked by one summer intern about the asynchronous code in our Angular project. He’d been staring at a service method for twenty minutes, convinced it was broken, when really it was just waiting patiently, one await at a time, for three requests that had no reason to block each other. I pulled up a blank file and said, “Let’s fix this together,” and by the end of the afternoon we’d walked through the five mistakes I see most often in async TypeScript. He asked good questions, so I wrote the answers down.

Async/await is the modern standard for asynchronous JavaScript/TypeScript, but misuse causes hard-to-debug issues: silent freezes, unhandled rejections, redundant concurrency, and over-scoped error handling. Here are 5 common async/await pitfalls in frontend production, paired with idiomatic Angular alternatives.

Pitfall 1: Unintentional Sequential Requests#

The Bug: Chaining independent await calls sequentially adds up latency instead of overlapping it.

// BAD: Sequential execution (~900ms total)
const getInfo = async () => {
const user = await getUser();
const list = await getList();
const banner = await getBanner();
};

The Fix: Run independent requests concurrently with Promise.all:

// GOOD: Concurrent execution (limited to the slowest request)
const getInfo = async () => {
const [user, list, banner] = await Promise.all([
getUser(),
getList(),
getBanner()
]);
};

The Angular Way: Angular relies on RxJS Observables via HttpClient. Use forkJoin to run HTTP requests in parallel:

import { forkJoin } from 'rxjs';
forkJoin({
user: this.http.get('/api/user'),
list: this.http.get('/api/list'),
banner: this.http.get('/api/banner')
}).subscribe(({ user, list, banner }) => {
// Executes in parallel, emits once all complete
});

Pitfall 2: All-or-Nothing Failures with Promise.all#

The Bug: Promise.all rejects immediately if any single request fails, blocking the whole page unnecessarily.

The Fix: Use Promise.allSettled to resolve all promises regardless of individual rejections, enabling partial UI rendering:

const results = await Promise.allSettled([
getUser(),
getList(),
getBanner()
]);

The Angular Way: In RxJS, an error on any inner stream terminates the combined stream. Intercept errors per request with catchError and return a safe fallback:

import { forkJoin, of } from 'rxjs';
import { catchError } from 'rxjs/operators';
forkJoin({
user: this.userService.getUser().pipe(catchError(err => of(null))),
list: this.listService.getList().pipe(catchError(err => of([]))),
banner: this.bannerService.getBanner().pipe(catchError(err => of(null)))
}).subscribe(data => {
// Partial failures won't break the entire page
});

Pitfall 3: Forgetting await (“Ghost Async Bugs”)#

The Bug: async functions always return a Promise. Omitting await yields the Promise wrapper, not the resolved value:

const getData = async () => request.get('/api/list');
// BAD: Assigns a Promise object instead of resolved data
const list = getData();

The Fix: Always await or .then() the invocation.

The Angular Way: Pass the Observable directly to the template using the async pipe, which handles subscription and unsubscription automatically:

<!-- Angular Preferred: Declarative unwrapping in templates -->
<div *ngIf="user$ | async as user">
{{ user.name }}
</div>

For imperative logic that needs a Promise (Angular v12+), use firstValueFrom:

import { firstValueFrom } from 'rxjs';
const list = await firstValueFrom(this.http.get<List>('/api/list'));

Pitfall 4: Over-Scoped try/catch Blocks#

The Bug: Wrapping multiple async calls in one monolithic try/catch obscures which request failed and blocks unrelated operations:

// BAD: Broad error boundary
try {
const user = await getUser();
const list = await getList();
} catch (err) {
showGlobalError('Request failed');
}

The Fix: Isolate try/catch boundaries per request to enable local degradation and clearer debugging.

The Angular Way: Use an HttpInterceptor for cross-cutting HTTP errors, and local RxJS catchError for component-level fallbacks:

this.userService.getUser().pipe(
catchError(error => {
this.notifier.showLocalWarning('User data unavailable');
return of(fallbackUser);
})
).subscribe();

Pitfall 5: Using await Inside Array.prototype.forEach#

The Bug: forEach is strictly synchronous and does not wait for promises inside its callback:

// BAD: Execution continues before loop async tasks finish
items.forEach(async (item) => {
await processItem(item);
});
console.log('Done'); // Logs immediately!

The Fix: Use a for...of loop for sequential async iteration, or map + Promise.all for parallel execution:

// Sequential
for (const item of items) {
await processItem(item);
}

The Angular Way: RxJS provides explicit concurrency operators for collections or streamed actions:

  • concatMap: sequential execution (equivalent to for...of + await)
  • mergeMap: parallel execution (equivalent to Promise.all)
  • switchMap: cancels the previous pending request when a new one arrives
import { from } from 'rxjs';
import { concatMap } from 'rxjs/operators';
// Process items sequentially in RxJS
from(items).pipe(
concatMap(item => this.http.post('/api/process', item))
).subscribe();

Summary Matrix#

Problem ScenarioVanilla JS SolutionAngular Standard (RxJS Idiom)
Independent Parallel RequestsPromise.all()forkJoin({...})
Fault-Tolerant RequestsPromise.allSettled()forkJoin + catchError(of(null))
Template Data Bindingawait fn()AsyncPipe (item$ | async)
Error IsolationLocal try/catchcatchError operator / HttpInterceptor
Sequential Iterationfor...of loopconcatMap operator
Avoid Async/Await Pitfalls in TypeScript
https://astro-nyc.pages.dev/posts/avoid-async-pitfall-typescript/
Author
Hari Seldon
Published at
2026-08-04
License
CC BY-NC-SA 4.0