Chapter 9.3โ˜• 15 min read

Subscriptions & Unsubscription

Managing subscriptions is critical in Angular. Unsubscribe to prevent memory leaks, zombie callbacks, and mysterious bugs.

01What is a Subscription

A Subscription is what you get back when you call .subscribe() on an Observable. It represents the execution and lets you cancel it.

import { Subscription } from 'rxjs';

// .subscribe() returns a Subscription object
const subscription = interval(1000).subscribe(v => console.log(v));

// Later โ€” cancel the subscription
subscription.unsubscribe(); // Stops the interval

// Check if already unsubscribed
console.log(subscription.closed); // true

"Subscription = newspaper subscription โ€” unsubscribe karo toh paper nahi aayega."

Key Subscription methods:

  • unsubscribe() โ€” cancel the Observable execution
  • closed โ€” boolean, true if unsubscribed
  • add(childSub) โ€” add child subscriptions โ€” calling unsubscribe() on parent unsubscribes all children

The add() method is especially useful โ€” you can collect multiple subscriptions and unsubscribe all at once.

02Why Unsubscribe is Critical

If you don't unsubscribe, the Observable keeps running even after the component is destroyed.

"Bina unsubscribe ke โ€” TV off karke bill bhi aa raha hai."

What happens when you forget to unsubscribe:

// BAD โ€” never unsubscribes!
export class BadComponent implements OnInit {
  ngOnInit() {
    interval(1000).subscribe(v => {
      console.log('Still running!', v);
      // This code runs even after component is destroyed!
    });
  }
  // No ngOnDestroy to clean up!
}

Three symptoms of subscription leaks:

  • Memory leak โ€” the callback function reference stays in memory
  • Zombie callbacks โ€” code tries to update a destroyed component's properties
  • "ExpressionChangedAfterItHasBeenCheckedError" โ€” weird Angular change detection errors

HTTP calls from HttpClient usually complete after one response, so they don't cause memory leaks. But it's still good practice to clean up.

03Old Way โ€” Manual Unsubscribe

The traditional way โ€” collecting subscriptions and unsubscribing in ngOnDestroy.

import { Component, OnInit, OnDestroy, inject } from '@angular/core';
import { Subscription, interval } from 'rxjs';
import { HttpClient } from '@angular/common/http';

export class OldStyleComponent implements OnInit, OnDestroy {
  private http = inject(HttpClient);

  // Collect all subscriptions in one Subscription object
  private subscriptions = new Subscription();

  ngOnInit() {
    // Add each subscription
    this.subscriptions.add(
      interval(1000).subscribe(v => console.log(v))
    );

    this.subscriptions.add(
      this.http.get('/api/biryani').subscribe(data => {
        this.biryanis = data;
      })
    );
  }

  ngOnDestroy() {
    // Unsubscribe ALL at once
    this.subscriptions.unsubscribe();
  }
}

"Manual = alag alag bills collect karo, ek saath pay karo."

Pros: Explicit, works in all Angular versions

Cons: Boilerplate, must remember to add to collection, forget once โ†’ memory leak

Still valid but the new ways are cleaner.

04New Way โ€” takeUntilDestroyed()

Angular 16+ introduced takeUntilDestroyed() โ€” the cleanest way to auto-unsubscribe.

import { Component, inject, DestroyRef } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { interval } from 'rxjs';

export class NewStyleComponent {
  private destroyRef = inject(DestroyRef);

  constructor() {
    interval(1000).pipe(
      takeUntilDestroyed(this.destroyRef) // Auto-cleanup!
    ).subscribe(v => console.log(v));
  }
  // No ngOnDestroy, no Subscription variable needed!
}

"takeUntilDestroyed = auto-debit โ€” khud ho jayega, tum fikar mat karo."

How it works:

  • takeUntilDestroyed(destroyRef) is an RxJS operator
  • When the component/cleanup context is destroyed, it automatically unsubscribes
  • No need for ngOnDestroy, no Subscription variable
  • Requires DestroyRef to be injected

โš ๏ธ Important: Must inject DestroyRef and pass it to takeUntilDestroyed(). Passing destroyRef ensures it works in all injection contexts.

05Other Auto-Cleanup Patterns

Several other patterns for automatic cleanup:

1. Async Pipe โ€” {{ data$ | async }}:

// Component
data$ = this.http.get('/api/biryani');

// Template โ€” auto subscribes AND unsubscribes on destroy
<div *ngFor="let biryani of data$ | async">
  {{ biryani.name }}
</div>

2. toSignal() โ€” Convert Observable to Signal:

import { toSignal } from '@angular/core/rxjs-interop';

data = toSignal(
  this.http.get('/api/biryani').pipe(catchError(() => of([]))),
  { initialValue: [] }
);
// No subscribe, no unsubscribe โ€” just a signal!

3. take(1) or first() โ€” Auto-complete after one value:

import { first } from 'rxjs/operators';

this.http.get('/api/biryani/42').pipe(
  first() // Auto-completes after first (and only) value
).subscribe(data => console.log(data));

"Jitne tareeqe hain utne options โ€” kaam karo jo easy lage."

Recommendation: For new code, prefer toSignal() for data (components) and takeUntilDestroyed() for side effects (logging, navigation). Use async pipe in templates for observables.

Key Takeaways

  • โœ… .subscribe() returns a Subscription โ€” .unsubscribe() cancels it, .closed checks state
  • โœ… Forgetting to unsubscribe = memory leak + zombie callbacks + weird errors
  • โœ… Old way: new Subscription() + .add(child) + ngOnDestroy unsubscribe
  • โœ… New way: takeUntilDestroyed(this.destroyRef) โ€” no ngOnDestroy needed
  • โœ… Other patterns: async pipe (template), toSignal() (reactive), take(1)/first() (auto-complete)
Course Search
Search across all chapters & stages
๐Ÿ“–

Search the course

Type any topic โ€” branching, stash, rebase, hooks โ€” and jump straight to that chapter.

merge branchesgit stashundo commitrebase