Introduction
RxJS subscriptions keep emitting values until unsubscribed. When Angular components subscribe but don't unsubscribe in ngOnDestroy, the subscription continues after component destruction, causing memory leaks and potential errors accessing destroyed component state.
Symptoms
Memory leak symptoms:
```typescript // Chrome DevTools Memory tab shows increasing DOM nodes // Performance degradation over time // Console warnings about destroyed component access
ERROR TypeError: Cannot read property 'value' of undefined at AppComponent.updateData (app.component.ts:45) // Called after component destroyed ```
Subscription leak:
WARNING: Component AppComponent has been destroyed but subscription still active.
Memory leak detected: 50 subscriptions not unsubscribed.Common Causes
- 1.Missing ngOnDestroy - No unsubscribe in destroy lifecycle
- 2.subscribe without unsubscribe - Direct subscribe() call
- 3.Multiple subscriptions - Each adds to memory
- 4.Event listeners - DOM events not removed
- 5.Timer/Interval - setInterval continues after destroy
- 6.Router events - Navigation events keep emitting
- 7.FormControl valueChanges - Form subscriptions leak
Step-by-Step Fix
Step 1: Identify Leaking Subscriptions
```typescript // Add ngOnDestroy to check if unsubscribed export class AppComponent implements OnDestroy { subscriptions: Subscription[] = [];
ngOnInit() { this.subscriptions.push( this.service.data$.subscribe(data => { console.log('Data received:', data); }) ); }
ngOnDestroy() { console.log('Active subscriptions:', this.subscriptions.length); // If > 0, leak detected } }
// Chrome DevTools Memory profiling: // 1. Take heap snapshot before navigating away // 2. Navigate to different route // 3. Take heap snapshot after // 4. Compare snapshots - look for detached DOM nodes ```
Step 2: Use takeUntil Pattern
```typescript import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators';
export class AppComponent implements OnDestroy { private destroy$ = new Subject<void>();
ngOnInit() { this.service.data$ .pipe(takeUntil(this.destroy$)) .subscribe(data => { this.data = data; });
this.http.get('/api/users') .pipe(takeUntil(this.destroy$)) .subscribe(users => { this.users = users; }); }
ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); // All subscriptions with takeUntil(this.destroy$) auto-unsubscribe } }
// Benefits: // - Single destroy subject for all subscriptions // - Cleaner than tracking multiple subscriptions // - Works with any observable ```
Step 3: Use Subscription Aggregation
```typescript import { Subscription } from 'rxjs';
export class AppComponent implements OnDestroy { private subscriptions = new Subscription();
ngOnInit() { this.subscriptions.add( this.service.data$.subscribe(data => { this.data = data; }) );
this.subscriptions.add( this.http.get('/api/users').subscribe(users => { this.users = users; }) );
this.subscriptions.add( this.router.events.subscribe(event => { // Handle router event }) ); }
ngOnDestroy() { this.subscriptions.unsubscribe(); // Unsubscribes all added subscriptions } }
// Benefits: // - Explicit subscription tracking // - Easy to add/remove individual subscriptions // - unsubscribe() handles all at once ```
Step 4: Use Async Pipe (Best Practice)
typescript
// Async pipe auto-subscribes and unsubscribes
@Component({
template:
<div>{{ data$ | async }}</div>
<div *ngIf="users$ | async as users">
<ul>
<li *ngFor="let user of users">{{ user.name }}</li>
</ul>
</div>
`
})
export class AppComponent {
// No need to subscribe manually
data$ = this.service.data$;
users$ = this.http.get('/api/users');
constructor( private service: DataService, private http: HttpClient ) {}
// No ngOnDestroy needed - async pipe handles cleanup }
// Benefits: // - Automatic subscription management // - No manual unsubscribe needed // - Better performance with OnPush ```
Step 5: Fix FormControl Subscriptions
```typescript // FormControl valueChanges leaks if not unsubscribed export class AppComponent implements OnInit, OnDestroy { private destroy$ = new Subject<void>(); form: FormGroup;
ngOnInit() { this.form = new FormGroup({ email: new FormControl('') });
// BAD: Leaks without unsubscribe this.form.valueChanges.subscribe(value => { console.log('Form changed:', value); });
// FIX: Use takeUntil this.form.valueChanges .pipe(takeUntil(this.destroy$)) .subscribe(value => { console.log('Form changed:', value); });
// Also for individual controls this.form.get('email')!.valueChanges .pipe(takeUntil(this.destroy$)) .subscribe(value => { console.log('Email changed:', value); }); }
ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); } } ```
Step 6: Fix Router Event Subscriptions
```typescript // Router events subscription leaks export class AppComponent implements OnInit, OnDestroy { private destroy$ = new Subject<void>();
ngOnInit() { // BAD: Router events never complete this.router.events.subscribe(event => { if (event instanceof NavigationStart) { console.log('Navigation started'); } });
// FIX: Use takeUntil this.router.events .pipe( takeUntil(this.destroy$), filter(event => event instanceof NavigationStart) ) .subscribe(event => { console.log('Navigation started'); }); }
ngOnDestroy() { this.destroy$.next(); } }
// Router events are infinite observables // Must unsubscribe explicitly ```
Step 7: Fix Event Listener Leaks
```typescript // DOM event listeners leak export class AppComponent implements OnInit, OnDestroy { private destroy$ = new Subject<void>();
ngOnInit() { // BAD: Direct event listener document.addEventListener('click', this.handleClick);
// FIX: Use fromEvent with takeUntil fromEvent(document, 'click') .pipe(takeUntil(this.destroy$)) .subscribe(event => { console.log('Document clicked'); });
// Or track listener manually this.clickHandler = this.handleClick.bind(this); document.addEventListener('click', this.clickHandler); }
ngOnDestroy() { // Remove listener manually document.removeEventListener('click', this.clickHandler);
// Or use destroy$ this.destroy$.next(); } } ```
Step 8: Fix Timer/Interval Leaks
```typescript // setInterval leaks export class AppComponent implements OnInit, OnDestroy { private destroy$ = new Subject<void>();
ngOnInit() { // BAD: setInterval without cleanup this.intervalId = setInterval(() => { this.updateTime(); }, 1000);
// FIX: Use interval observable interval(1000) .pipe(takeUntil(this.destroy$)) .subscribe(() => { this.updateTime(); });
// Or use ngOnDestroy this.intervalId = setInterval(() => { this.updateTime(); }, 1000); }
ngOnDestroy() { if (this.intervalId) { clearInterval(this.intervalId); } this.destroy$.next(); } } ```
Step 9: Use take(1) for Single Emit
```typescript // For observables that should emit once ngOnInit() { // HTTP call - completes after one emit // But still good practice to unsubscribe
// Use take(1) for clarity this.http.get('/api/users') .pipe(take(1)) .subscribe(users => { this.users = users; });
// first() operator also works this.service.data$ .pipe(first()) .subscribe(data => { this.initialData = data; });
// These complete after first value, no leak // But takeUntil is safer pattern } ```
Step 10: Monitor Memory Usage
```typescript // Add memory tracking in development export class AppComponent implements OnDestroy { ngOnInit() { console.log('Component initialized'); console.log('Heap size:', (performance as any).memory?.usedJSHeapSize); }
ngOnDestroy() { console.log('Component destroyed'); console.log('Heap size:', (performance as any).memory?.usedJSHeapSize);
// Check if heap size decreased // If not, may have memory leak } }
// Use Chrome DevTools: // 1. Open Memory tab // 2. Take heap snapshot // 3. Navigate away from component // 4. Take another snapshot // 5. Compare - look for AppComponent still in memory ```
Subscription Cleanup Patterns
| Pattern | Use Case | Pros |
|---|---|---|
| takeUntil | Multiple subscriptions | Single cleanup point |
| Subscription.add | Explicit tracking | Easy add/remove |
| async pipe | Template bindings | Auto cleanup, best |
| take(1) | Single emit | No cleanup needed |
Verification
```typescript // After implementing proper unsubscribe
// 1. Open Chrome DevTools Memory tab // 2. Load component // 3. Take heap snapshot // 4. Navigate away (destroy component) // 5. Take another heap snapshot // 6. Compare - component should NOT be in memory
// 7. Check console for ngOnDestroy calls ngOnDestroy() { console.log('AppComponent destroyed'); this.destroy$.next(); }
// Should see log on navigation
// 8. Performance test // Navigate back and forth multiple times // Memory should stay stable, not grow
// 9. Check detached DOM nodes // Memory snapshot > Detached DOM nodes // Should be minimal (< 10)
// 10. Run production build ng build --prod // App should run without memory warnings ```
Prevention
To prevent Angular memory leak subscription issues from recurring, implement these proactive measures:
1. Use Async Pipe When Possible
// Prefer async pipe over manual subscription
@Component({
template: `<div>{{ data$ | async }}</div>`
})
export class SafeComponent {
data$: Observable<string> = this.service.getData();
// No subscription needed, no cleanup needed
}2. Implement Base Component Pattern
```typescript // Base component with automatic cleanup export abstract class BaseComponent implements OnDestroy { protected destroy$ = new Subject<void>();
ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); }
protected takeUntilDestroy<T>() { return takeUntil<T>(this.destroy$); } }
// Usage export class UserComponent extends BaseComponent { ngOnInit() { this.service.getData() .pipe(this.takeUntilDestroy()) .subscribe(data => this.data = data); } } ```
3. Use ESLint RxJS Rules
// .eslintrc.json
{
"extends": ["plugin:rxjs/recommended"],
"rules": {
"rxjs/no-ignored-subscription": "warn",
"rxjs/no-implicit-any-catch": "warn",
"rxjs/no-nested-subscribe": "error"
}
}Best Practices Checklist
- [ ] Use async pipe when possible
- [ ] Implement base component pattern
- [ ] Enable RxJS ESLint rules
- [ ] Test memory usage in DevTools
- [ ] Use takeUntil pattern
- [ ] Clean up timers and intervals
Related Issues
- [Fix Angular Change Detection Loop](/articles/fix-angular-change-detection-loop)
- [Fix Angular ExpressionChangedAfterChecked](/articles/fix-angular-expressionchangedafterchecked)
- [Fix RxJS Observable Not Emitting](/articles/fix-rxjs-observable-not-emitting)
Related Articles
- [Technical troubleshooting: Fix Browser Back Button State Lost Spa Navigation ](browser-back-button-state-lost-spa-navigation)
- [Fix Cors Fetch Localhost Development Blocked Issue in Frontend](cors-fetch-localhost-development-blocked)
- [Fix Csp Violation Blocked Inline Script Style Issue in Frontend](csp-violation-blocked-inline-script-style)
- [Fix Angular Change Detection Loop](fix-angular-change-detection-loop)
- [Fix Angular Dependency Injection Error](fix-angular-dependency-injection-error)
<script type="application/ld+json"> { "@context": "https://schema.org", "@type": "TechArticle", "headline": "Fix Angular Memory Leak Subscription", "description": "Troubleshoot Angular memory leaks from subscriptions. Use takeUntil, async pipe, or ngOnDestroy to unsubscribe.", "url": "https://www.fixwikihub.com/fix-angular-memory-leak-subscription", "publisher": { "@type": "Organization", "name": "FixWikiHub", "url": "https://www.fixwikihub.com" }, "author": { "@type": "Person", "name": "FixWikiHub Editorial Team" }, "datePublished": "2026-04-04T01:30:49.159Z", "dateModified": "2026-04-04T01:30:49.159Z" } </script>