Avoid Nested Subscriptions
Replaces nested RxJS subscriptions with composable operators and lifecycle-safe subscription handling in Angular code.
SKILL.mdskills/angular/rxjs/avoid-nested-subscriptions/SKILL.md
GitHubAvoid Nested Subscriptions
Purpose
Use this skill to replace subscription pyramids with clear RxJS composition. The goal is to prevent leaks, race conditions, duplicated requests, hidden ordering bugs, and hard-to-test asynchronous flows.
When to Use
Use this skill when:
- A
subscribecall appears inside anothersubscribe. - A request depends on the result of a previous observable.
- Multiple observable values must be combined before updating state.
- The code manually stores many subscriptions.
- Component lifecycle cleanup is missing or unclear.
- The task mentions RxJS leaks, duplicated API calls, or async race conditions.
Do
Use switchMap when a new source value should cancel the previous inner operation:
readonly userDetails$ = this.route.paramMap.pipe(
map((params) => params.get('id')),
filter((id): id is string => id !== null),
switchMap((id) => this.usersService.getUserDetails(id)),
);
Use combineLatest when multiple ongoing sources define the current view state:
readonly viewModel$ = combineLatest([
this.user$,
this.permissions$,
]).pipe(
map(([user, permissions]) => ({ user, permissions })),
);
Use forkJoin when independent one-time operations must complete together:
forkJoin({
profile: this.usersService.getProfile(userId),
settings: this.settingsService.getSettings(userId),
}).subscribe(({ profile, settings }) => {
this.profile.set(profile);
this.settings.set(settings);
});
Use concatMap when operations must run sequentially and order matters:
saveClicks
.pipe(concatMap((payload) => this.ordersService.save(payload)))
.subscribe();
For search or route changes, verify which source emission reaches switchMap. An upstream debounce or filter can delay or prevent cancellation. In a search test, emit an old response after the replacement request starts and assert that it is ignored; also assert teardown when cancellation itself is required. For write operations, do not choose switchMap merely to remove nesting if cancellation would lose intended work.
If a failed inner request should not stop future input, handle its error inside the flattening operator. Test a failure followed by success through the same subscription, rather than recreating the stream after the error.
In Angular components, prefer async in templates or use takeUntilDestroyed for imperative subscriptions:
this.form.valueChanges
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((value) => this.formValue.set(value));
Do Not
Avoid nested subscriptions:
this.route.paramMap.subscribe((params) => {
const id = params.get("id");
this.usersService.getUserDetails(id).subscribe((user) => {
this.permissionsService
.getPermissions(user.role)
.subscribe((permissions) => {
this.viewModel = { user, permissions };
});
});
});
Avoid ignoring lifecycle cleanup for long-lived streams:
this.form.valueChanges.subscribe((value) => {
this.formValue = value;
});
Review Checklist
- No
subscribecall is nested inside anothersubscribe. -
switchMapis used for cancelable dependent work. -
concatMapis used for ordered sequential work. -
mergeMapis used only when concurrent inner work is intended. -
combineLatestis used for ongoing combined state. -
forkJoinis used for one-time parallel completion. - Component subscriptions use
asyncortakeUntilDestroyed. - Error handling remains explicit after the refactor.
- Cancellation, ordering, and continued operation after errors are tested at the public boundary.
Expected Output
- Identify nested subscriptions and lifecycle risks.
- Determine the correct RxJS operator based on cancellation, ordering, and combination semantics.
- Refactor the observable chain without changing behavior.
- Preserve explicit error and loading handling.
- Add or update tests when async behavior changes.