skip to content

RxJS Usage Patterns

Where Angular's APIs meet RxJS: Observable-returning APIs, the async pipe, takeUntilDestroyed and toSignal bridges. Interviewers probe subscription leaks, duplicate requests and caching.

part ofAngularoverview, primer and where to startread it →
on this pageshow

explore

questions

27

In Angular, what does the async pipe do over a component's lifetime when its template binds `notifications$ | async`?

level: juniorimportance: must knowfreq 80%

answer

  1. starts on first evaluation, not construction
  2. latest value, returned synchronously
  3. markForCheck on each async emission
  4. new reference means dispose and resubscribe
  5. ngOnDestroy unsubscribes

basics

~20 s

Angular's async pipe subscribes to the Observable or Promise when the binding is first evaluated, returns its latest value, calls markForCheck on each asynchronous emission, swaps subscriptions if the reference changes, and unsubscribes when the view is destroyed.

solid answer

~40 s

`AsyncPipe` (from `@angular/common`, imported into a standalone component's `imports`) is an impure pipe (`pure: false`) whose `transform` runs on every check. The first time it sees a non-null Observable or Promise it subscribes and returns the latest value it holds, which is `null` until something arrives. Each asynchronous emission stores the value and calls `ChangeDetectorRef.markForCheck()`, so the view is refreshed even under `OnPush` (the default for new components since v22) and in a zoneless app. If the expression later yields a different reference, the pipe disposes the old subscription and subscribes to the new one. When the view is destroyed, `ngOnDestroy` unsubscribes, so the component writes no teardown code for that stream.

code

ts · 20 lines
ts
import { Component, inject } from '@angular/core';
import { AsyncPipe } from '@angular/common';
import { HttpClient } from '@angular/common/http';

interface Notification { id: number; text: string; }

@Component({
  selector: 'app-notification-feed',
  imports: [AsyncPipe],
  template: `
    @for (n of notifications$ | async; track n.id) {
      <p>{{ n.text }}</p>
    }
  `,
})
export class NotificationFeed {
  private http = inject(HttpClient);
  // Created once; the async pipe subscribes on first check and unsubscribes on destroy.
  readonly notifications$ = this.http.get<Notification[]>('/api/notifications');
}

go deeper

for a junior

Recall the three jobs: subscribe, return the latest value, unsubscribe when the view goes away. Remember to import AsyncPipe into a standalone component.

for a middle

Explain the impure transform, markForCheck on asynchronous emissions only, the reset to null when the bound reference changes, and why each | async is its own subscription.

for a senior

Tie the pipe to OnPush-by-default and zoneless applications, and spot where a template's pipes multiply subscriptions or never subscribe because their block never renders.

for a principal

Weigh template-owned subscriptions against toSignal-based components for a codebase's conventions, considering migration cost, testability and consistency across teams.

## What the async pipe is `AsyncPipe` is a built-in pipe exported from `@angular/common` under the template name `async`. In a standalone component you add `AsyncPipe` to the component's `imports` array (importing `CommonModule` also works). It accepts an `Observable`, any **Subscribable** (an object with a `subscribe` method), a `Promise` or `PromiseLike`, or `null`/`undefined`. Anything else throws an `InvalidPipeArgument` runtime error. The pipe is declared with `pure: false`. An **impure pipe** has its `transform` method called on every change-detection pass of the view that contains it, not only when its input reference changes. That is what lets it hand back a value that changed inside the stream while the stream object itself stayed the same. Every place in a template that writes `| async` gets **its own pipe instance**. Two bindings mean two instances and two subscriptions, which is the root of the duplicate-request questions interviewers like to ask. ## The lifecycle, step by step 1. **Creation.** The pipe instance is created with the view. It holds no subscription yet, and its stored latest value is `null`. 2. **First evaluation.** During the view's first check, `transform(notifications$)` sees it has no source, subscribes, and returns whatever the latest value is at that moment. For a source that emits synchronously on subscribe (a `BehaviorSubject`, `of(...)`, a `startWith`), that is already the real value; for an HTTP call or a timer it is `null`. 3. **Asynchronous emission.** When the source emits later, the pipe stores the value and calls `markForCheck()` on the view's `ChangeDetectorRef`. That marks the view and its ancestors dirty and notifies Angular's change-detection scheduler, so the next pass calls `transform` again and it returns the new value. 4. **Reference change.** If a later check passes a *different* Observable or Promise, the pipe disposes the old subscription, resets the stored value to `null`, and subscribes to the new source. 5. **Destruction.** When the view is destroyed (the component is removed, an `@if` block turns false, an `@for` row disappears), `ngOnDestroy` unsubscribes and drops its reference to the `ChangeDetectorRef`. Two details are easy to miss. A value delivered **synchronously during the subscribe call** does not trigger `markForCheck`, because it is returned from the same `transform` call anyway. And a binding inside a block that never renders never subscribes at all: the pipe lives in that block's view. ## Why the markForCheck matters in current Angular Since v22, a component that does not set `changeDetection` is `OnPush`, and since v21 new applications are zoneless by default. In that world nothing re-checks a view "just because" an HTTP response arrived. The async pipe's `markForCheck()` is exactly the notification Angular needs: it marks the view for check and schedules a pass. A component that subscribed manually and assigned the value to a plain field would stay stale under those defaults unless it also called `markForCheck()` or stored the value in a signal. ## Promises are handled too For a `Promise`, the pipe attaches `then` callbacks. A Promise cannot be cancelled, so on destroy the pipe simply **drops its callbacks**; if the Promise resolves later, the result is ignored and the destroyed view is not touched. ## Compared with subscribing in the class | Concern | `obs$ \| async` in the template | `subscribe()` in the class | |---|---|---| | Subscribes | on first evaluation of the binding | wherever you call it | | Marks the view for check | automatically, per async emission | you call `markForCheck()` or write a signal | | Unsubscribes | on view destroy, or when the reference changes | you arrange it yourself | | Value before the first emission | `null` | whatever you initialised the field to | | Errors | sent to the application's `ErrorHandler` | your `error` callback | ## What interviewers listen for - That the pipe is **impure** and runs on every check, but only subscribes once per source reference. - That it **marks for check**, which is why it works with `OnPush` and zoneless apps without extra code. - That it **unsubscribes on destroy** and when the bound reference changes. - That each `| async` is a **separate subscription**, so a cold source such as an `HttpClient` call runs once per binding. - That the first value is often **`null`**, and the template has to allow for it. In new code many teams convert streams to signals with `toSignal()` instead, but the async pipe remains fully supported and is everywhere in existing codebases.

  • Does the async pipe subscribe when the component is constructed?
    No. It subscribes inside `transform`, during the first change-detection pass that evaluates the binding. A binding inside an `@if` or `@defer` block that has not rendered has no pipe instance yet, so nothing subscribes until that block's view is created.
  • Why does the async pipe work in an `OnPush` component without extra code?
    On each asynchronous emission it calls `markForCheck()` on the view's `ChangeDetectorRef`, which marks the view and its ancestors dirty and notifies the change-detection scheduler, so the next pass re-reads the binding. Values delivered synchronously during subscribe are returned directly without marking.
  • What does the async pipe do with a Promise when the view is destroyed first?
    It cannot cancel the Promise, so it discards the callbacks it attached. When the Promise settles later, the result is ignored and the destroyed view is never updated.

It is like a newspaper subscription tied to a flat's lease: it starts when someone moves in (the view renders), each delivery rings the doorbell so the resident looks (markForCheck), and moving out cancels it automatically (the view is destroyed).

saying these in an interview costs you the question

  • The async pipe subscribes in the component constructor
  • The async pipe is pure, so it only runs when the input reference changes
  • You still need takeUntilDestroyed for streams bound with the async pipe
  • The async pipe only works with Default or Eager change detection
  • Two async pipes on the same Observable share one subscription
open as a page

In an Angular observable data service, why keep the BehaviorSubject private and expose state through asObservable() instead?

level: juniorimportance: must knowfreq 66%

basics

~20 s

A private BehaviorSubject means only the service's own methods can emit new state, so every change goes through one path; asObservable() gives consumers an Observable backed by the subject that has no next(), error() or complete() to call.

open as a page

In an Angular component, how does `toSignal()` expose a WebSocket price feed to the template, and when does it subscribe and unsubscribe?

level: juniorimportance: must knowfreq 70%

basics

~10 s

Angular's toSignal() subscribes to the Observable as soon as it is called, stores each emission in a read-only signal the template reads with (), and unsubscribes when the calling component's or service's DestroyRef fires.

open as a page

In Angular, why does calling HttpClient.get() without subscribing send no request, and why does subscribing to the result twice send two?

level: juniorimportance: must knowfreq 62%

basics

~20 s

HttpClient methods return cold Observables: the call only describes the request, and each subscription runs the interceptor chain and backend again, sending a new request. With the default observe: 'body' each run emits one body and completes.

open as a page

In Angular, how does `takeUntilDestroyed()` from `@angular/core/rxjs-interop` end a subscription, and where can you call it without an argument?

level: juniorimportance: must knowfreq 75%

basics

~20 s

Angular's takeUntilDestroyed() pipes the source through takeUntil with a notifier fired by a DestroyRef's onDestroy callback, so the stream completes when the component, directive or injector is destroyed; without an argument it must run in an injection context.

open as a page

How do you make an Angular service load a feature-flag config through HttpClient once and share the cached response with every component?

level: middleimportance: must knowfreq 62%

basics

~20 s

Store http.get(url).pipe(shareReplay(1)) in a service field so every caller gets the same shared Observable: the first subscriber fires one request, later subscribers receive the replayed response, and a failed request is retried by the next subscriber.

open as a page

In Angular, what does `toSignal(obs$)` return before the first emission, and how do the `initialValue` and `requireSync` options change it?

level: middleimportance: must knowfreq 60%

basics

~10 s

Angular's toSignal() returns undefined until the first emission, typed Signal<T | undefined>; initialValue supplies a value for that gap, and requireSync: true removes it by requiring a synchronous emission, throwing NG0601 otherwise.

open as a page

In Angular, how do the Observables from HttpClient, Router.events, ActivatedRoute.paramMap and a form control's valueChanges differ in emissions and completion?

level: middleimportance: must knowfreq 58%

basics

~20 s

An HttpClient call is cold: each subscription sends a request, emits one response body and completes. Router.events, ActivatedRoute.paramMap and valueChanges are shared, long-lived streams that never complete; paramMap replays the current params on subscribe, while the other two only emit future events.

open as a page

In an Angular component, which subscriptions end on their own and which need teardown: HttpClient calls, `Router.events`, a FormControl's `valueChanges`, and RxJS `interval()`?

level: middleimportance: must knowfreq 65%

basics

~10 s

In Angular, HttpClient calls complete after the response, but Router.events, valueChanges and RxJS interval() never complete, so their subscriptions outlive the component unless tied to DestroyRef, the async pipe or toSignal.

open as a page

An Angular header pipes `notifications$ = http.get(...)` through `| async` for a badge and again for a list; why does the Network tab show two requests, and how do you fix it?

level: seniorimportance: must knowfreq 62%

basics

~10 s

Each | async in an Angular template is its own AsyncPipe subscription, and an HttpClient Observable sends a new request per subscription, so two bindings send two requests; subscribe once and reuse the value.

open as a page

In an Angular template, what does `@if (unread$ | async; as count)` give you, and why can it hide a legitimate zero?

level: middleimportance: should knowfreq 52%

basics

~20 s

Angular's @if (unread$ | async; as count) subscribes once and exposes the value as count inside the block, but the block renders only for truthy values, so null before loading and a real 0 both hide it.

open as a page

In Angular, why does `unreadCount$ | async` evaluate to null on the first check, and when does it return a real value immediately?

level: middleimportance: should knowfreq 58%

basics

~20 s

Angular's async pipe returns its stored latest value, which starts as null; it becomes real on the first pass only if the source emits synchronously while being subscribed, as a BehaviorSubject, of() or startWith() source does.

open as a page

In current Angular, how would you rewrite a BehaviorSubject-based data service with signals, and what changes for the components that consume it?

level: middleimportance: should knowfreq 55%

basics

~10 s

Hold state in a private signal(), expose it with asReadonly(), derive values with computed(), and write through set()/update() with new references; consumers read service.flags() directly, with no subscription, async pipe or teardown.

open as a page

In Angular, what does `outputFromObservable()` from `@angular/core/rxjs-interop` do, and how does it handle subscription, errors and completion?

level: middleimportance: should knowfreq 30%

basics

~10 s

Angular's outputFromObservable() declares a component or directive output whose events come from an Observable; each listener's subscription is forwarded to the source, errors are not handled, completion stops emissions, and forwarding stops on destroy.

open as a page

In Angular, why does `toObservable(symbol)` emit only the last of three synchronous `symbol.set()` calls, and when does it emit at all?

level: middleimportance: should knowfreq 48%

basics

~10 s

Angular's toObservable() forwards a signal's value through an effect into a ReplaySubject(1); the effect runs later and reads only the current value, so several synchronous set() calls produce one emission of the last value.

open as a page

In Angular reactive forms, what do a control's valueChanges, statusChanges and events Observables emit, and when does each one fire?

level: middleimportance: should knowfreq 44%

basics

~20 s

valueChanges emits the new value and statusChanges the recalculated status each time a control updates, from the UI or code, with no initial value; events emits typed ControlEvent objects for value, status, touched, pristine, submit and reset. None of them completes.

open as a page

In an Angular product-detail page, when does ActivatedRoute.paramMap emit, and why does that matter when navigating from /products/1 to /products/2?

level: middleimportance: should knowfreq 50%

basics

~20 s

paramMap emits the current params synchronously on subscribe and emits again whenever the params change while the component instance is reused; it never completes. Moving from /products/1 to /products/2 reuses the component, so only paramMap, not a one-time snapshot read, sees the new id.

open as a page

In an Angular component, why does calling `takeUntilDestroyed()` inside `ngOnInit` or a click handler throw NG0203, and how do you fix it?

level: middleimportance: should knowfreq 55%

basics

~10 s

Angular's takeUntilDestroyed() without an argument injects DestroyRef, which is only allowed in an injection context; ngOnInit and event handlers run later, so it throws NG0203. Inject DestroyRef into a field and pass it in.

open as a page

In Angular, why does binding `loadNotifications() | async`, where the method returns `this.http.get(...)`, send request after request?

level: seniorimportance: should knowfreq 48%

basics

~20 s

Angular calls the method on every change-detection pass, each call returns a new Observable, and the async pipe treats a new reference as a new source, so it unsubscribes, resubscribes and sends another request each time.

open as a page

An Angular service polls feature flags with timer, switchMap and shareReplay(1), and polling continues after every subscribing component is destroyed; why, and what does refCount: true change?

level: seniorimportance: should knowfreq 46%

basics

~20 s

shareReplay's refCount defaults to false, so its internal ReplaySubject stays subscribed to the polling source after the last subscriber leaves; shareReplay({ bufferSize: 1, refCount: true }) unsubscribes the source at zero subscribers and restarts it on the next subscription.

open as a page

In Angular, why does calling `toSignal()` in `ngOnInit` throw NG0203, and why does calling it inside `computed()` or a template getter throw NG0602?

level: seniorimportance: should knowfreq 45%

basics

~10 s

Angular's toSignal() injects DestroyRef for cleanup, so outside an injection context it throws NG0203; inside a reactive context each re-evaluation would create a new subscription, so development mode throws NG0602.

open as a page

In Angular, what does EventEmitter inherit from RxJS Subject, and what goes wrong when a service exposes one as a shared event bus?

level: seniorimportance: should knowfreq 30%

basics

~20 s

EventEmitter extends RxJS Subject and adds emit(): it is hot, has no current value and never completes on its own. Exposed from a service, it lets every consumer emit, error or complete it, which the Subject-based API cannot prevent.

open as a page

In an Angular codebase, how does the `takeUntil(this.destroy$)` teardown pattern work, where does it go wrong, and how does `takeUntilDestroyed` replace it?

level: seniorimportance: should knowfreq 50%

basics

~10 s

In Angular's older pattern a component keeps a private Subject, pipes streams through takeUntil(this.destroy$) and calls destroy$.next() in ngOnDestroy; takeUntilDestroyed does the same through DestroyRef without the Subject or hook.

open as a page

An Angular edit dialog subscribes to its form's `valueChanges` to autosave; after opening it ten times, one keystroke sends ten saves. What is happening, and how do you fix it?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Each dialog instance left a valueChanges subscriber behind, because the form outlives the dialog or the dialog is never destroyed; tie the subscription to the dialog's DestroyRef with takeUntilDestroyed and make sure closing calls ComponentRef.destroy().

open as a page

When does an Angular app outgrow BehaviorSubject or signal-based data services, and how would you decide whether a store library such as NgRx is warranted?

level: principalimportance: should knowfreq 38%

basics

~20 s

Services suffice while each slice of state has one owning service and changes stay traceable; a store library pays off when many features write shared state and changes and side effects need one auditable home.

open as a page

In Angular, what happens when an Observable bound with the async pipe errors, and how do you render an error state instead?

level: middleimportance: nice to knowfreq 35%

basics

~20 s

Angular's async pipe reports the error to the application's ErrorHandler; the errored subscription is over and the binding keeps its last value or null, so render an error state by catching the error in the stream and mapping it to a value.

open as a page

In a zoneless Angular app with server-side rendering, what does `pendingUntilEvent()` from `@angular/core/rxjs-interop` do, and when would you add it?

level: seniorimportance: nice to knowfreq 15%

basics

~10 s

Angular's pendingUntilEvent() operator adds a PendingTasks task when the Observable is subscribed and removes it at the first next, complete, error or unsubscribe, keeping the app unstable so server rendering waits for that value.

open as a page