AngularJSThe Async Pipe

The Async Pipe

The AsyncPipe is one of Angular's most important and frequently used pipes. It handles the entire lifecycle of subscribing to an Observable or resolving a Promise — and critically, it automatically unsubscribes when the component is destroyed, eliminating a common source of memory leaks.

Why the Async Pipe Exists

Without the async pipe, you would need to manually subscribe, store the value, and unsubscribe in every component that consumes async data:

TS
// Without async pipe — verbose and error-prone
@Component({ template: `<p>{{ userName }}</p>` })
export class UserComponent implements OnInit, OnDestroy {
  userName: string = '';
  private sub!: Subscription;

  constructor(private userService: UserService) {}

  ngOnInit() {
    this.sub = this.userService.getUser().subscribe(user => {
      this.userName = user.name;
    });
  }

  ngOnDestroy() {
    this.sub.unsubscribe(); // forget this and you have a memory leak
  }
}

TS
// With async pipe — clean, safe, automatic
@Component({
  standalone: true,
  imports: [AsyncPipe],
  template: `<p>{{ userName$ | async }}</p>`
})
export class UserComponent {
  userName$ = this.userService.getUser().pipe(
    map(user => user.name)
  );

  constructor(private userService: UserService) {}
}
Basic Usage

Import AsyncPipe from @angular/common in standalone components:

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

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

@Component({
  selector: 'app-user-profile',
  standalone: true,
  imports: [AsyncPipe],
  template: `
    <div>{{ user$ | async | json }}</div>
  `,
})
export class UserProfileComponent {
  user$: Observable<User>;

  constructor(private http: HttpClient) {
    this.user$ = this.http.get<User>('/api/user/1');
  }
}
Handling Loading and Error States

The async pipe returns null before the observable emits. Use Angular's @if control flow to handle loading states:

HTML
<!-- Pattern 1: Loading indicator -->
@if (user$ | async; as user) {
  <div class="profile">
    <h2>{{ user.name }}</h2>
    <p>{{ user.email }}</p>
  </div>
} @else {
  <div class="spinner">Loading...</div>
}

<!-- Pattern 2: Combined loading + error + data -->
@if (data$ | async; as data) {
  <ul>
    @for (item of data; track item.id) {
      <li>{{ item.name }}</li>
    }
  </ul>
}

For proper error handling, transform your observable in the component using RxJS operators:

TS
import { catchError, of } from 'rxjs';

interface State<T> {
  data: T | null;
  loading: boolean;
  error: string | null;
}

@Component({
  standalone: true,
  imports: [AsyncPipe],
  template: `
    @if (state$ | async; as state) {
      @if (state.loading) { <p>Loading...</p> }
      @if (state.error) { <p class="error">{{ state.error }}</p> }
      @if (state.data) {
        @for (item of state.data; track item.id) {
          <div>{{ item.name }}</div>
        }
      }
    }
  `,
})
export class DataListComponent {
  state$ = this.http.get<Item[]>('/api/items').pipe(
    map(data => ({ data, loading: false, error: null })),
    catchError(err => of({ data: null, loading: false, error: err.message })),
    startWith({ data: null, loading: true, error: null }),
  );

  constructor(private http: HttpClient) {}
}
Multiple Subscriptions — The Share Problem

Every use of the async pipe in a template creates a separate subscription. This can cause problems with HTTP requests — each subscription triggers a new HTTP call:

HTML
<!-- PROBLEM: This creates TWO HTTP requests -->
@if ((user$ | async)?.name) {
  <h2>{{ (user$ | async)?.name }}</h2>
  <p>{{ (user$ | async)?.email }}</p>
}

HTML
<!-- SOLUTION: Use the "as" syntax to subscribe once -->
@if (user$ | async; as user) {
  <h2>{{ user.name }}</h2>
  <p>{{ user.email }}</p>
}

TS
// Or share the observable at the source
import { shareReplay } from 'rxjs/operators';

export class UserComponent {
  // shareReplay(1) caches the last value and multicasts
  user$ = this.http.get<User>('/api/user').pipe(
    shareReplay(1)
  );
}
Warning
Always use the as syntax in templates, or apply shareReplay(1) to the source observable to avoid triggering multiple HTTP requests.
Async Pipe with Promises

The async pipe works equally well with native Promises:

TS
@Component({
  standalone: true,
  imports: [AsyncPipe],
  template: `
    @if (userData | async; as user) {
      <p>{{ user.name }}</p>
    }
  `,
})
export class UserComponent {
  userData: Promise<User> = fetch('/api/user/1').then(r => r.json());
}
Note
Unlike Observables, Promises cannot be cancelled and always produce a single value. Prefer Observables for HTTP requests in Angular to get cancellation, retry, and composability.
Async Pipe and Change Detection

One of the most powerful aspects of the async pipe is its deep integration with Angular's change detection system. When a component uses ChangeDetectionStrategy.OnPush, the async pipe still triggers change detection when new values arrive — without any extra boilerplate:

TS
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { AsyncPipe } from '@angular/common';

@Component({
  selector: 'app-live-prices',
  standalone: true,
  imports: [AsyncPipe],
  // OnPush: only re-renders when inputs change or async pipe emits
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    @for (stock of stocks$ | async; track stock.symbol) {
      <div>{{ stock.symbol }}: {{ stock.price | number: '1.2-2' }}</div>
    }
  `,
})
export class LivePricesComponent {
  stocks$ = this.marketService.getLivePrices();
  constructor(private marketService: MarketService) {}
}
Tip
Combining AsyncPipe with ChangeDetectionStrategy.OnPush is the recommended pattern for high-performance Angular components — components only re-render when the observable emits.
Async Pipe in Forms

The async pipe is also useful for populating form dropdown options from an API:

TS
@Component({
  standalone: true,
  imports: [AsyncPipe, ReactiveFormsModule],
  template: `
    <select [formControl]="countryControl">
      <option value="">Select a country...</option>
      @for (country of countries$ | async; track country.code) {
        <option [value]="country.code">{{ country.name }}</option>
      }
    </select>
  `,
})
export class CountrySelectComponent {
  countryControl = new FormControl('');
  countries$ = this.geoService.getCountries();

  constructor(private geoService: GeoService) {}
}
Comparing Async Pipe vs Manual Subscribe

Aspect

Async Pipe

Manual subscribe()

Unsubscription

Automatic on destroy

Manual (risk of leak)

Boilerplate

Minimal

ngOnInit + ngOnDestroy + Subscription

Change Detection

Integrated with OnPush

Need manual markForCheck()

Multiple subscriptions

Each | async is a new subscription

Full control

Error handling

Requires RxJS operators in stream

Easy in subscribe() callback

Testability

Easy with TestScheduler

Easy

Common Patterns and Recipes

HTML
<!-- Pattern: Combine multiple streams with combineLatest -->
@if ({ user: user$ | async, posts: posts$ | async }; as vm) {
  @if (vm.user && vm.posts) {
    <h2>{{ vm.user.name }}</h2>
    <p>{{ vm.posts.length }} posts</p>
  }
}

TS
// Better: combine in component with combineLatest
import { combineLatest, map } from 'rxjs';

export class ProfileComponent {
  vm$ = combineLatest({
    user: this.userService.user$,
    posts: this.postService.posts$,
    notifications: this.notifyService.count$,
  });
}

HTML
@if (vm$ | async; as vm) {
  <h2>{{ vm.user.name }}</h2>
  <p>{{ vm.posts.length }} posts</p>
  <span>{{ vm.notifications }} notifications</span>
}
Summary
  • AsyncPipe subscribes to Observables and Promises and unwraps the value in the template

  • Automatically unsubscribes on component destruction — no memory leaks

  • Returns null before the first emission — handle with @if and the "as" syntax

  • Each pipe usage in a template is a separate subscription — use "as" or shareReplay(1) to avoid duplicate requests

  • Works seamlessly with ChangeDetectionStrategy.OnPush for optimal performance

  • Prefer it over manual subscribe() in component classes for cleaner, safer code