Skip to content
Angular ng rxjs 4 min read

Subscribing & Unsubscribing

An observable is lazy — it does nothing until you subscribe. Subscribing kicks off the producer (a timer, an HTTP request, a stream of events) and registers callbacks that run as values arrive. But every active subscription holds resources, and if you never tear it down it keeps running after the component is gone — the classic Angular memory leak. This page covers how to subscribe, what the Subscription object gives you, and the modern strategies for guaranteed cleanup.

Subscribing to an observable

Calling .subscribe() activates the stream. You can pass either three positional callbacks (the legacy form) or a single observer object with next, error, and complete handlers. The object form is clearer and is the recommended style today.

import { interval } from 'rxjs';
import { take } from 'rxjs/operators';

interval(1000).pipe(take(3)).subscribe({
  next: (v) => console.log('value', v),
  error: (err) => console.error('failed', err),
  complete: () => console.log('done'),
});

Output:

value 0
value 1
value 2
done

Each call to subscribe creates an independent execution of the observable. Subscribe twice and the producer function runs twice — two timers, two HTTP requests. This is what “cold observable” means, and it is why you should share a single subscription rather than calling subscribe repeatedly.

The Subscription object

subscribe() returns a Subscription — a handle representing the ongoing execution. Its most important method is unsubscribe(), which runs the observable’s teardown logic (clearing timers, removing listeners, aborting requests) and stops further emissions.

import { interval } from 'rxjs';

const sub = interval(500).subscribe((v) => console.log('tick', v));

// Stop after 1.2 seconds
setTimeout(() => sub.unsubscribe(), 1200);

Output:

tick 0
tick 1

Subscriptions compose. You can call sub.add(otherSub) to group children under a parent, so a single parent.unsubscribe() tears down all of them at once.

const group = new Subscription();
group.add(stream1$.subscribe(/* … */));
group.add(stream2$.subscribe(/* … */));

// Cancels both at once
group.unsubscribe();

A stream that completes or errors unsubscribes itself automatically — of(), finite take() pipelines, and a single HttpClient request all clean up on their own. The leak risk is with infinite streams: interval, fromEvent, route params, valueChanges, and BehaviorSubject. Those need explicit teardown.

Why manual unsubscribe is fragile

Storing every subscription and unsubscribing in ngOnDestroy works, but it is noisy and easy to forget:

export class StaleComponent implements OnDestroy {
  private subs: Subscription[] = [];

  ngOnInit() {
    this.subs.push(this.service.data$.subscribe(/* … */));
  }

  ngOnDestroy() {
    this.subs.forEach((s) => s.unsubscribe());
  }
}

One missed push, one early return, and you leak. Angular offers two far better patterns: takeUntilDestroyed and the async pipe.

takeUntilDestroyed

takeUntilDestroyed (from @angular/core/rxjs-interop) is an operator that completes the stream when the component or directive is destroyed. Called inside a constructor or field initializer it picks up the injection context automatically; elsewhere, pass a DestroyRef.

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

@Component({
  selector: 'app-clock',
  standalone: true,
  template: `<p>Ticks: {{ count }}</p>`,
})
export class ClockComponent {
  private readonly destroyRef = inject(DestroyRef);
  count = 0;

  constructor() {
    interval(1000)
      .pipe(takeUntilDestroyed(this.destroyRef))
      .subscribe(() => this.count++);
  }
}

When the component is destroyed the source completes, the subscription tears down, and you wrote zero cleanup code. This is the preferred way to subscribe imperatively in modern Angular.

The async pipe

When the value is destined for the template, skip subscribe entirely. The async pipe subscribes for you, renders the latest value, and unsubscribes when the host view is destroyed — leak-proof by construction. It pairs naturally with the new @if and @for control flow.

import { Component } from '@angular/core';
import { AsyncPipe } from '@angular/common';
import { interval, map } from 'rxjs';

@Component({
  selector: 'app-async-clock',
  standalone: true,
  imports: [AsyncPipe],
  template: `
    @if (elapsed$ | async; as seconds) {
      <p>Elapsed: {{ seconds }}s</p>
    }
  `,
})
export class AsyncClockComponent {
  elapsed$ = interval(1000).pipe(map((n) => n + 1));
}

Using as seconds subscribes once and binds the result to a local variable, avoiding the multiple-subscription trap of piping the same observable several times in a template.

Choosing a strategy

StrategyCleanupBest for
Manual unsubscribe()You call it in ngOnDestroyOne-off non-Angular code, dynamic subscriptions
Subscription.add()One unsubscribe() cancels the groupBundling several manual subscriptions
takeUntilDestroyed()Automatic on destroyImperative subscriptions inside components/directives
async pipeAutomatic with the viewValues rendered directly in the template

Best Practices

  • Prefer the async pipe whenever the value goes to the template — it never leaks and removes boilerplate.
  • For imperative subscriptions inside a component, reach for takeUntilDestroyed instead of hand-managing ngOnDestroy.
  • Use the observer-object form of subscribe and always handle the error channel so failures are not swallowed silently.
  • Remember finite streams complete on their own; only infinite sources (interval, fromEvent, valueChanges, subjects) need explicit teardown.
  • Avoid subscribing to the same observable multiple times in a template; bind once with | async as value.
  • Never nest subscribe calls — use flattening operators like switchMap instead to keep teardown automatic.
Last updated June 14, 2026
Was this helpful?