Skip to content
Angular ng pipes 4 min read

Async Pipe

The async pipe is one of Angular’s most useful built-in pipes. It subscribes to an Observable or Promise directly in the template, returns the latest emitted value, and—crucially—unsubscribes automatically when the component is destroyed. This eliminates an entire class of memory-leak bugs that come from manual subscribe() calls and forgotten teardown logic, while keeping your component classes thin and declarative.

How the async pipe works

When you write expression | async, Angular subscribes to the source on your behalf. Each time the source emits, the pipe marks the component for change detection and unwraps the new value into the view. When the component is destroyed—or when the expression reference changes—the pipe unsubscribes from the old source and subscribes to the new one. You never write .subscribe() and you never write a corresponding unsubscribe().

import { Component, inject } from '@angular/core';
import { AsyncPipe } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

interface User {
  id: number;
  name: string;
}

@Component({
  selector: 'app-user-list',
  standalone: true,
  imports: [AsyncPipe],
  template: `
    @if (users$ | async; as users) {
      <ul>
        @for (user of users; track user.id) {
          <li>{{ user.name }}</li>
        }
      </ul>
    } @else {
      <p>Loading users…</p>
    }
  `,
})
export class UserListComponent {
  private http = inject(HttpClient);
  users$: Observable<User[]> = this.http.get<User[]>('/api/users');
}

The as users syntax captures the unwrapped value into a template variable so you can reference it multiple times without re-subscribing. The @else branch renders while the value is still null (the initial state before the first emission).

Tip: Subscribe to a source only once per template. If you write {{ data$ | async }} in several places, each usage creates its own subscription—and for cold observables like HttpClient.get(), that means duplicate HTTP requests. Use @if (data$ | async; as data) and reuse data.

Why it prevents memory leaks

A manual subscription that is never torn down keeps the component (and everything it references) alive after Angular tries to destroy it. The async pipe ties the subscription lifecycle to the view, so teardown is guaranteed.

// Manual approach — easy to leak, more boilerplate
export class TimerComponent implements OnInit, OnDestroy {
  count = 0;
  private sub?: Subscription;

  ngOnInit() {
    this.sub = interval(1000).subscribe((n) => (this.count = n));
  }

  ngOnDestroy() {
    this.sub?.unsubscribe(); // forget this line → leak
  }
}
// Async pipe approach — no OnDestroy, no Subscription field
@Component({
  selector: 'app-timer',
  standalone: true,
  imports: [AsyncPipe],
  template: `<p>Ticks: {{ count$ | async }}</p>`,
})
export class TimerComponent {
  count$ = interval(1000);
}

Observables vs. promises

The async pipe transparently handles both. The behavioral differences are summarized below.

AspectObservablePromise
EmissionsZero or many over timeExactly one resolution
Initial valuenull until first emitnull until resolved
UnsubscribePipe unsubscribes on destroyNothing to unsubscribe
Re-subscriptionOn reference changeOn reference change
@Component({
  selector: 'app-greeting',
  standalone: true,
  imports: [AsyncPipe],
  template: `<h2>{{ greeting | async }}</h2>`,
})
export class GreetingComponent {
  greeting: Promise<string> = Promise.resolve('Hello from a promise!');
}

Combining with the signals world

In modern Angular you can bridge RxJS and signals with toSignal(), which removes the need for the async pipe entirely in many cases. The async pipe remains the right tool when you are working directly with observable streams in a template.

import { Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';

@Component({
  selector: 'app-status',
  standalone: true,
  template: `<p>Online users: {{ count() ?? 0 }}</p>`,
})
export class StatusComponent {
  private http = inject(HttpClient);
  count = toSignal(this.http.get<number>('/api/online-count'));
}

Output:

Online users: 42

Gotcha: Do not call a method that creates an observable inside the template (e.g. {{ getUsers() | async }}). Change detection runs the method repeatedly, producing a brand-new observable each cycle, which the pipe re-subscribes to endlessly. Assign the observable to a field once.

Error handling

The async pipe does not catch errors—an errored stream throws and breaks the binding. Handle errors inside the stream with catchError so the pipe always receives a valid value.

users$ = this.http.get<User[]>('/api/users').pipe(
  catchError(() => of([])) // fall back to an empty list
);

Best Practices

  • Prefer the async pipe over manual subscribe()/unsubscribe() to guarantee teardown and reduce boilerplate.
  • Subscribe once with @if (source$ | async; as value) and reuse value instead of piping the same source repeatedly.
  • Add catchError (or similar) to streams so a failed emission does not break the template binding.
  • Never invoke a method that builds an observable inside the template—assign the stream to a class field.
  • Use shareReplay(1) on cold HTTP observables when the same data must feed several template branches.
  • Reach for toSignal() when you want signal-based reactivity instead of pipe-based unwrapping.
Last updated June 14, 2026
Was this helpful?