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:
// 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
}
}// 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:
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:
<!-- 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:
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:
<!-- PROBLEM: This creates TWO HTTP requests -->
@if ((user$ | async)?.name) {
<h2>{{ (user$ | async)?.name }}</h2>
<p>{{ (user$ | async)?.email }}</p>
}<!-- SOLUTION: Use the "as" syntax to subscribe once -->
@if (user$ | async; as user) {
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
}// 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)
);
}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:
@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());
}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:
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) {}
}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:
@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
<!-- 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>
}
}// 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$,
});
}@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