Skip to content
Angular ng rxjs 4 min read

takeUntilDestroyed

Manual subscription management is one of the most common sources of memory leaks in Angular applications. Forget to unsubscribe from a long-lived observable and it keeps running after the component is gone, holding references and firing callbacks against destroyed views. The takeUntilDestroyed operator, introduced in Angular 16, solves this cleanly: it completes a subscription automatically when the surrounding component, directive, or injection context is destroyed — no ngOnDestroy, no Subscription arrays, no boilerplate.

What takeUntilDestroyed does

takeUntilDestroyed is an RxJS operator exported from @angular/core/rxjs-interop. It wires the observable’s lifecycle to Angular’s DestroyRef. When the associated context is destroyed, the operator emits a completion to the stream, which tears down the subscription the same way takeUntil would with a manual notifier subject.

Because it depends on the framework’s dependency injection, it must be called in an injection context — typically a constructor, a field initializer, or inside a runInInjectionContext callback. When used there, it discovers the current DestroyRef automatically.

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

@Component({
  selector: 'app-ticker',
  standalone: true,
  template: `<p>Ticks: {{ count }}</p>`,
})
export class TickerComponent {
  count = 0;

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

When TickerComponent is removed from the DOM, the interval subscription completes automatically.

Why it replaces manual cleanup

Before takeUntilDestroyed, the idiomatic pattern was a destroy subject:

import { Component, OnDestroy } from '@angular/core';
import { interval, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

@Component({ /* ... */ })
export class OldTickerComponent implements OnDestroy {
  private destroy$ = new Subject<void>();

  ngOnInit() {
    interval(1000)
      .pipe(takeUntil(this.destroy$))
      .subscribe(() => { /* ... */ });
  }

  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  }
}

The new operator collapses all of that into a single .pipe(takeUntilDestroyed()) call.

ConcernManual takeUntil(destroy$)takeUntilDestroyed()
Extra Subject fieldRequiredNone
ngOnDestroy boilerplateRequiredNone
Risk of forgetting cleanupHighNone
Works outside componentsNoYes (any DI context)
Operator position in pipeAnywhereShould be last

Using it outside an injection context

If you subscribe later — for example, inside an event handler or after ngOnInit has run — you are no longer in an injection context, so the operator cannot resolve DestroyRef on its own. Capture the DestroyRef while you still have access to DI and pass it explicitly.

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

@Component({
  selector: 'app-search',
  standalone: true,
  template: `<input #box placeholder="Search…" />`,
})
export class SearchComponent {
  private destroyRef = inject(DestroyRef);

  startListening(input: HTMLInputElement) {
    fromEvent(input, 'input')
      .pipe(takeUntilDestroyed(this.destroyRef))
      .subscribe((e) => console.log((e.target as HTMLInputElement).value));
  }
}

Tip: Inject DestroyRef as a field at construction time, then reuse it anywhere in the class. Calling inject(DestroyRef) itself only works inside an injection context.

Behavior on completion

takeUntilDestroyed completes the stream — it does not error. Any complete callbacks run, and finalize operators fire, giving you a hook for resource cleanup.

import { Component } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { interval } from 'rxjs';
import { finalize } from 'rxjs/operators';

@Component({ selector: 'app-demo', standalone: true, template: '' })
export class DemoComponent {
  constructor() {
    interval(500)
      .pipe(
        finalize(() => console.log('stream finalized — cleanup here')),
        takeUntilDestroyed(),
      )
      .subscribe((n) => console.log('value', n));
  }
}

Output:

value 0
value 1
value 2
stream finalized — cleanup here

The finalize runs the moment the component is destroyed, after the last buffered value.

Operator ordering

Place takeUntilDestroyed as the last operator in the pipe whenever possible. Operators after it will not see the completion in the order you might expect, and putting it last guarantees the entire pipeline shuts down together. The one common exception is pairing it with finalize, where finalize typically goes just before it so its callback still fires on teardown.

Best Practices

  • Reach for takeUntilDestroyed() instead of hand-rolled destroy$ subjects or Subscription arrays — it is the modern, leak-proof default.
  • Prefer the no-argument form inside constructors and field initializers where the injection context is available automatically.
  • Capture inject(DestroyRef) once as a class field and pass it explicitly for subscriptions created after construction.
  • Keep takeUntilDestroyed last in the pipe so the whole stream tears down cleanly.
  • For one-shot streams like HttpClient calls that complete on their own, you usually don’t need it — use it for long-lived sources (interval, fromEvent, BehaviorSubject, route params).
  • Where the data is for the template only, consider the async pipe or signals first; takeUntilDestroyed shines when you need an imperative subscription with side effects.
Last updated June 14, 2026
Was this helpful?