DEV Community

Hitesh Gharate
Hitesh Gharate

Posted on

Fixing Angular race conditions with switchMap and shareReplay

Your search box works fine in dev. In production, users type fast, the network is uneven, and suddenly the results on screen don't match what they typed.

That's a race condition, and RxJS gives us clean tools to fix it.

The problem

Here's a typical typeahead search:

this.searchControl.valueChanges.subscribe(term => {
  this.api.search(term).subscribe(results => {
    this.results = results;
  });
});
Enter fullscreen mode Exit fullscreen mode

Type "ang", then "angular". Both requests fly out. If the "ang" response arrives last, it overwrites the right results. Nested subscribes also leak and are hard to clean up.

Fix 1: switchMap cancels stale requests

this.results$ = this.searchControl.valueChanges.pipe(
  switchMap(term => this.api.search(term))
);
Enter fullscreen mode Exit fullscreen mode

When a new value arrives, switchMap unsubscribes from the previous inner request and cancels it. Only the latest request can ever emit.

Fix 2: reduce noise with debounceTime and distinctUntilChanged

this.results$ = this.searchControl.valueChanges.pipe(
  debounceTime(300),
  distinctUntilChanged(),
  switchMap(term => this.api.search(term))
);
Enter fullscreen mode Exit fullscreen mode
  • debounceTime(300) waits until the user pauses typing.
  • distinctUntilChanged() skips repeats of the same value.

This means fewer API calls and less load on your backend.

Fix 3: handle errors without killing the stream

If the HTTP call errors, the whole stream dies and search stops working. Catch the error inside the inner observable:

switchMap(term =>
  this.api.search(term).pipe(
    catchError(() => of([]))
  )
)
Enter fullscreen mode Exit fullscreen mode

Second race: duplicate requests with multiple subscribers

Say a component template reads the same observable twice:

<div *ngIf="(user$ | async) as user">{{ user.name }}</div>
<div>{{ (user$ | async)?.email }}</div>
Enter fullscreen mode Exit fullscreen mode

Each async pipe subscribes separately, so you get two HTTP calls, which can return different data at different times.

Fix 4: shareReplay shares one request

user$ = this.http.get<User>('/api/user').pipe(
  shareReplay({ bufferSize: 1, refCount: true })
);
Enter fullscreen mode Exit fullscreen mode

Now all subscribers share one request, and late subscribers get the last cached value.

Why refCount: true? Without it, the source subscription stays alive even after all subscribers leave, which can cause leaks. With it, the source is released when nobody is listening.

Putting it together

@Component({ /* ... */ })
export class SearchComponent {
  searchControl = new FormControl('', { nonNullable: true });

  results$ = this.searchControl.valueChanges.pipe(
    debounceTime(300),
    distinctUntilChanged(),
    switchMap(term =>
      this.api.search(term).pipe(catchError(() => of([])))
    ),
    shareReplay({ bufferSize: 1, refCount: true })
  );

  constructor(private api: SearchService) {}
}
Enter fullscreen mode Exit fullscreen mode
<input [formControl]="searchControl" />
<ul>
  <li *ngFor="let r of results$ | async">{{ r.name }}</li>
</ul>
Enter fullscreen mode Exit fullscreen mode

No manual subscriptions, no cleanup code, and no stale results.

When NOT to use switchMap

switchMap cancels in-flight work, which is perfect for reads but risky for writes. Pick the operator by intent:

Operator Use for
switchMap Searches, autocomplete, latest-wins reads
concatMap Writes that must run in order
exhaustMap Ignore repeat clicks, such as a submit button
mergeMap Independent parallel work

Using switchMap on a save request could cancel a write mid-flight, so use concatMap or exhaustMap there.

Key takeaways

  • Nested subscribes cause race conditions and leaks.
  • switchMap gives you "latest wins" for reads.
  • debounceTime and distinctUntilChanged cut unnecessary calls.
  • shareReplay({ bufferSize: 1, refCount: true }) prevents duplicate requests.
  • Choose the flattening operator based on what the request does.

I've used these patterns on large Angular applications, and they remove a whole class of "works on my machine" bugs. What race condition have you run into? Let me know in the comments.

Top comments (0)