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 dataconst 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 boundarytry { 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 finishitems.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:
// Sequentialfor (const item of items) { await processItem(item);}The Angular Way: RxJS provides explicit concurrency operators for collections or streamed actions:
concatMap: sequential execution (equivalent tofor...of+await)mergeMap: parallel execution (equivalent toPromise.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 RxJSfrom(items).pipe( concatMap(item => this.http.post('/api/process', item))).subscribe();Summary Matrix
| Problem Scenario | Vanilla JS Solution | Angular Standard (RxJS Idiom) |
|---|---|---|
| Independent Parallel Requests | Promise.all() | forkJoin({...}) |
| Fault-Tolerant Requests | Promise.allSettled() | forkJoin + catchError(of(null)) |
| Template Data Binding | await fn() | AsyncPipe (item$ | async) |
| Error Isolation | Local try/catch | catchError operator / HttpInterceptor |
| Sequential Iteration | for...of loop | concatMap operator |