Skip to content

Angular Async Pipe

The async pipe is the cleanest way to render Observable or Promise values directly in an Angular template, without manually subscribing in the component class. This lesson covers how it works and common patterns around it.

What Does the Async Pipe Do?

The async pipe subscribes to an Observable (or Promise) passed into it, and returns the most recently emitted value, updating the template automatically as new values arrive. When the component is destroyed, it also automatically unsubscribes, no manual cleanup code needed.

This removes an entire category of boilerplate: no ngOnInit subscription, no class property to store the latest value, and no ngOnDestroy cleanup.

@Component({
  selector: 'app-user-list',
  standalone: true,
  imports: [AsyncPipe],
  template: `
    <ul>
      @for (user of (users$ | async); track user.id) {
        <li>{{ user.name }}</li>
      }
    </ul>
  `,
})
export class UserListComponent {
  private userService = inject(UserService);
  users$ = this.userService.getUsers();
}

No subscription or unsubscription code appears anywhere in this component, the async pipe manages the entire lifecycle.

Async Pipe Syntax

{{ observable$ | async }}
*ngIf="observable$ | async as value"
@if (observable$ | async; as value) { {{ value }} }
  • The basic form {{ obs$ | async }} renders the latest emitted value directly.
  • Combining async with as value (via *ngIf or the modern @if) lets you name the resolved value for reuse elsewhere in the same block.
  • The pipe returns null before the first value has arrived, template code should handle that case.
  • AsyncPipe must be imported in a standalone component's imports array to use it.

Async Pipe Cheatsheet

Common patterns for using the async pipe effectively.

Pattern Example
Basic rendering {{ data$ | async }}
Alias with @if @if (data$ | async; as data) { {{ data.name }} }
Alias with *ngIf *ngIf="data$ | async as data"
Looping over async data @for (x of (list$ | async); track x.id)
Handling null before first emission {{ (data$ | async)?.name ?? 'Loading...' }}
Multiple uses of the same stream Alias once with as, reuse the alias

Avoiding Multiple Subscriptions to the Same Stream

Using {{ data$ | async }} multiple times in the same template creates a separate subscription (and, for a fresh HTTP call, a separate request) each time. Aliasing the resolved value once with as avoids this duplication.

<!-- avoid: subscribes twice -->
<p>{{ (user$ | async)?.name }}</p>
<p>{{ (user$ | async)?.email }}</p>

<!-- better: subscribes once, reused via alias -->
@if (user$ | async; as user) {
  <p>{{ user.name }}</p>
  <p>{{ user.email }}</p>
}

Async Pipe vs Manual Subscription

Manual subscription (storing a value on the class and updating it in a callback) still has valid uses, when you need to trigger side effects, combine multiple streams manually, or perform logic beyond simple display. For pure rendering, the async pipe is almost always simpler and safer.

Approach Best For
async pipe Directly rendering a stream's value in the template
Manual .subscribe() Side effects, logic beyond display, combining multiple sources imperatively

Async Pipe vs toSignal()

toSignal() offers a signal-based alternative to the async pipe: convert an Observable to a signal once in the class, then read it in the template like any other signal, without needing the async pipe or as aliasing pattern at all.

users = toSignal(this.userService.getUsers(), { initialValue: [] });

<!-- template, no async pipe needed -->
@for (user of users(); track user.id) {
  <li>{{ user.name }}</li>
}

Common Mistakes

  • Piping the same Observable through async multiple times in one template instead of aliasing it once with as.
  • Forgetting that the async pipe returns null before the first emission, causing template errors on properties of that null value.
  • Manually subscribing and storing a class property when the async pipe (or toSignal()) would eliminate that boilerplate entirely.
  • Not importing AsyncPipe in a standalone component that uses it.

Key Takeaways

  • The async pipe subscribes to an Observable/Promise in the template and automatically unsubscribes on destroy.
  • Aliasing a resolved value with as avoids creating multiple subscriptions to the same stream.
  • The pipe returns null before the first emission, template code should account for that.
  • toSignal() is a signal-based alternative that avoids the async pipe and as aliasing pattern entirely.

Pro Tip

When a template needs the same async value in more than one place, always alias it once with @if (data$ | async; as data) rather than repeating data$ | async, this is one of the most common, easy-to-miss Angular performance mistakes.