Skip to content
Angular interview 4 min read

RxJS Interview Questions

RxJS is the reactive backbone of Angular: HttpClient, the Router, forms, and the async pipe all speak Observables. Interviewers probe RxJS because subtle misuse — forgetting to unsubscribe, choosing the wrong flattening operator, or treating Observables like Promises — causes the most common bugs in production Angular apps. The questions below cover the comparisons and gotchas that come up most often, with modern Angular 17–19 idioms throughout.

How do Observables differ from Promises?

This is the canonical opener. The key distinctions are lazy versus eager, single versus multiple values, and cancellable versus not.

A Promise runs its executor the moment it is created and resolves exactly once. An Observable is lazy — nothing happens until you subscribe() — and it can emit zero, one, or many values over time. Crucially, an Observable subscription is cancellable via unsubscribe(), which is how Angular aborts in-flight HTTP requests when a component is destroyed.

AspectPromiseObservable
ExecutionEager (runs immediately)Lazy (runs on subscribe)
ValuesSingle0…n over time
CancellableNoYes (unsubscribe)
Operators.then/.catch only100+ pipeable operators
Re-executionNoYes (each subscribe re-runs)
import { Observable } from 'rxjs';

const obs$ = new Observable<number>((subscriber) => {
  console.log('producer runs');
  subscriber.next(1);
  subscriber.next(2);
  subscriber.complete();
});

console.log('before subscribe'); // nothing has run yet
obs$.subscribe((v) => console.log('got', v));

Output:

before subscribe
producer runs
got 1
got 2

The producer logging after “before subscribe” demonstrates laziness — a frequent follow-up question. A Promise would have logged first.

What are the flattening operators and when do you use each?

When an outer Observable emits values that themselves trigger inner Observables (e.g., a search box that fires an HTTP request per keystroke), you need a flattening operator to manage the inner subscriptions. The choice determines concurrency and cancellation behaviour — a guaranteed interview question.

OperatorStrategyUse case
switchMapCancels the previous inner ObservableType-ahead search, latest-wins reads
mergeMapRuns all inner Observables concurrentlyIndependent parallel writes
concatMapQueues inner Observables in orderOrdered, sequential writes
exhaustMapIgnores new values while one is activeLogin buttons, debounce-by-ignore
import { Component, inject, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs';
import { toSignal } from '@angular/core/rxjs-interop';

@Component({
  selector: 'app-search',
  standalone: true,
  imports: [ReactiveFormsModule],
  template: `
    <input [formControl]="query" placeholder="Search users" />
    @for (user of results(); track user.id) {
      <p>{{ user.name }}</p>
    }
  `,
})
export class SearchComponent {
  private http = inject(HttpClient);
  query = new FormControl('', { nonNullable: true });

  results = toSignal(
    this.query.valueChanges.pipe(
      debounceTime(300),
      distinctUntilChanged(),
      switchMap((term) =>
        this.http.get<{ id: number; name: string }[]>(`/api/users?q=${term}`),
      ),
    ),
    { initialValue: [] as { id: number; name: string }[] },
  );
}

Here switchMap cancels the previous HTTP request when a new keystroke arrives, so a slow earlier response can never overwrite a newer one. Using mergeMap instead would cause out-of-order results.

What are Subjects, and how do the variants differ?

A Subject is both an Observable and an Observer — you can subscribe to it and push values into it with next(). This makes Subjects the standard tool for multicasting and for event-bus-style communication between unrelated components.

  • Subject — has no initial value; late subscribers miss prior emissions.
  • BehaviorSubject — requires a seed value and replays the latest value to new subscribers. Ideal for state.
  • ReplaySubject — replays the last n emissions to new subscribers.
  • AsyncSubject — emits only the final value, and only on completion.
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class CartService {
  private itemCount = new BehaviorSubject<number>(0);
  readonly itemCount$ = this.itemCount.asObservable();

  add(): void {
    this.itemCount.next(this.itemCount.value + 1);
  }
}

Expose asObservable() rather than the raw Subject so consumers can’t call next() and corrupt your state — a detail interviewers like to hear.

How do you prevent memory leaks from subscriptions?

A leaked subscription keeps a component alive in memory and continues running side effects after the view is gone. There are several idiomatic fixes; mentioning more than one signals depth.

  1. Prefer the async pipe — Angular subscribes and unsubscribes for you automatically.
  2. Use toSignal() — the modern interop helper ties the subscription to the injection context and cleans up on destroy.
  3. takeUntilDestroyed() — the cleanest manual approach in Angular 16+.
import { Component, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { interval } from 'rxjs';

@Component({ selector: 'app-ticker', standalone: true, template: '' })
export class TickerComponent {
  constructor() {
    interval(1000)
      .pipe(takeUntilDestroyed())
      .subscribe((n) => console.log('tick', n));
  }
}

When called in a constructor, takeUntilDestroyed() infers the DestroyRef automatically; outside an injection context you pass one in explicitly. This replaces the older takeUntil(this.destroy$) boilerplate.

Best practices

  • Reach for the async pipe or toSignal() first — manual .subscribe() should be the exception, not the default.
  • Choose flattening operators deliberately: switchMap for reads, concatMap/exhaustMap for writes you must not duplicate or reorder.
  • Seed shared state with BehaviorSubject and expose it via asObservable() to keep writes encapsulated.
  • Always pair manual subscriptions with takeUntilDestroyed() so cleanup is impossible to forget.
  • Debounce and distinctUntilChanged() user-driven streams to avoid redundant work.
  • Keep side effects in tap() and data transforms in map() so pipelines stay readable and testable.
Last updated June 14, 2026
Was this helpful?