interviewDeck

Your one-stop platform to prepare, practice and ace your interviews.

Loading your questions…

All Questions

Filters & tools

RxJS Interview Questions and Answers

29 hand-picked RxJS interview questions with detailed answers. Open the interactive version above to search, filter by difficulty, run code, bookmark questions and track your progress.

Observable vs Promise.

An Observable is a lazy stream of values that starts executing only when subscribed. It can emit zero, one, or many values over time and supports cancellation through unsubscribe(). A Promise is eager, starts executing immediately when created, resolves or rejects only once, and cannot be cancelled. Observables also provide powerful RxJS operators such as map, filter, switchMap, and catchError, making them ideal for Angular applications.

// Promise
const promise = fetch('/api/users')
  .then(res => res.json());

promise.then(users => console.log(users));

// Observable
this.http.get<User[]>('/api/users')
  .subscribe(users => console.log(users));

switchMap vs mergeMap vs concatMap vs exhaustMap.

These are called flattening operators because they transform an Observable that emits Observables (Observable<Observable<T>>) into a single Observable. The difference lies in how they handle multiple inner Observables.

  • switchMap — Cancels the previous inner Observable when a new value arrives. Best for search/autocomplete and route changes.
  • mergeMap — Runs all inner Observables concurrently without cancellation. Best for independent parallel requests.
  • concatMap — Queues requests and executes them one after another, preserving order. Best for sequential operations like saving records.
  • exhaustMap — Ignores new emissions while the current Observable is still running. Best for preventing duplicate actions such as login or payment requests.
// switchMap - Search Box
this.searchControl.valueChanges.pipe(
  debounceTime(300),
  distinctUntilChanged(),
  switchMap(term => this.userService.search(term))
).subscribe();

Subject vs BehaviorSubject vs ReplaySubject vs AsyncSubject.

Subject is both an Observable and an Observer. It multicasts values to multiple subscribers, but subscribers only receive values emitted after they subscribe.

BehaviorSubject stores the latest value, requires an initial value, and immediately emits the current value to new subscribers.

ReplaySubject stores and replays the last N emitted values to new subscribers.

AsyncSubject emits only the last value, and only after the Observable completes.

const subject = new Subject<number>();
const behavior = new BehaviorSubject<number>(0);
const replay = new ReplaySubject<number>(2);
const asyncSubject = new AsyncSubject<number>();

How do you avoid memory leaks / unsubscribe?

  • async pipe in the template — subscribes and unsubscribes automatically (preferred).
  • takeUntil(destroy$) with a Subject completed in ngOnDestroy.
  • takeUntilDestroyed() in modern Angular.
  • Store the Subscription and call unsubscribe() in ngOnDestroy.

HTTP calls complete on their own, but long-lived streams (Subjects, valueChanges, intervals) leak if you don't clean up.

this.data$.pipe(takeUntil(this.destroy$)).subscribe(...);
ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); }

Hot vs cold observables.

Cold — each subscriber triggers its own producer, getting an independent execution (e.g. an HTTP call runs per subscribe).

Hot — the producer is shared; subscribers see values as they happen (e.g. DOM events, a Subject). Use share() / shareReplay() to make a cold stream hot.

readonly config$ = this.http.get('/config').pipe(shareReplay(1));

Name common RxJS operators and what they do.

RxJS operators are functions used inside pipe() to transform, filter, combine, or control Observables. Common operators include map, filter, tap, debounceTime, distinctUntilChanged, switchMap, mergeMap, concatMap, take, takeUntil, catchError, and forkJoin. They are the building blocks of reactive programming in Angular.

this.searchControl.valueChanges.pipe(
  debounceTime(300),
  distinctUntilChanged(),
  switchMap(value => this.userService.search(value)),
  catchError(() => of([]))
).subscribe(users => {
  this.users = users;
});

How do you handle errors in RxJS?

Use catchError to intercept an error and return a fallback observable (or rethrow). retry(n) / retryWhen re-subscribe on failure.

Remember: an error terminates the stream — nothing emits after it, so handle it inside the inner observable if you want the outer stream to survive.

switchMap(term => this.api.search(term).pipe(
  catchError(() => of([]))
))

combineLatest vs forkJoin vs merge vs zip.

combineLatest emits the latest value from each Observable whenever any source emits (after all have emitted at least once). forkJoin waits for all Observables to complete and emits their last values once (similar to Promise.all()). merge emits values from all Observables as they arrive without waiting. zip combines values by their emission order (index), waiting until each Observable has a corresponding value.

// forkJoin Example
forkJoin({
  user: this.userService.getUser(),
  roles: this.userService.getRoles()
}).subscribe(({ user, roles }) => {
  console.log(user, roles);
});

of vs from vs fromEvent.

  • of(1,2,3) — emits the given values then completes.
  • from(arrayOrPromise) — turns an array/iterable/Promise into an observable.
  • fromEvent(el,'click') — an observable of DOM events.
of([1, 2, 3]);   // one emission: [1,2,3]
from([1, 2, 3]); // three emissions: 1, 2, 3

What is the pipe() method?

pipe() is a method on an Observable that lets you chain multiple RxJS operators together. Each operator receives the output of the previous one and returns a new Observable without modifying the original stream. This makes data transformation, filtering, error handling, and asynchronous operations easy to compose in a readable way.

this.http.get<User[]>('/api/users').pipe(
  map(users => users.filter(u => u.active)),
  tap(() => console.log('Users loaded')),
  catchError(() => of([]))
).subscribe(users => {
  console.log(users);
});

Why pair debounceTime with distinctUntilChanged in search?

debounceTime waits for the user to pause typing; distinctUntilChanged drops emissions equal to the previous one (e.g. type then delete back to the same term) so you don't fire a duplicate request.

What does startWith do?

startWith() emits one or more values immediately when an Observable is subscribed to, before the source Observable starts emitting its own values. It is commonly used to provide an initial value so the UI has something to display while waiting for real data.

this.items$ = this.http.get<Item[]>('/api/items').pipe(
  startWith([])
);

this.searchControl.valueChanges.pipe(
  startWith('')
).subscribe(console.log);

Show the canonical type-ahead search pipeline.

Combine the four operators every interviewer wants: debounceTime (wait for a pause), distinctUntilChanged (skip duplicate terms), switchMap (cancel stale requests), and catchError inside the inner call (so one failure doesn't kill the stream).

input$.pipe(
  debounceTime(300),
  distinctUntilChanged(),
  switchMap(q => this.api.search(q).pipe(catchError(() => of([]))))
)

How do you poll an API every N seconds with RxJS?

Use timer(0, N) to emit immediately then every N ms, and switchMap to the request (so a slow response doesn't overlap the next tick). Stop with takeUntil.

timer(0, 5000).pipe(
  switchMap(() => this.api.getStatus()),
  takeUntil(this.destroy$)
)

How do you retry a failed request with exponential backoff?

Use retry with a delay function (RxJS 7.4+) that returns an increasing delay per attempt, or retryWhen in older versions. Cap the number of retries so it doesn't loop forever.

req$.pipe(
  retry({ count: 3, delay: (_e, i) => timer(2 ** i * 1000) })
)

How do you cache an HTTP result across subscribers?

Pipe the request through shareReplay(1): the underlying call runs once, and every current and future subscriber gets the last emitted value — a one-line in-memory cache.

The buffer size 1 means "replay the most recent value". Without shareReplay, each subscriber re-runs the cold HTTP call.

readonly config$ = this.http.get('/config').pipe(shareReplay(1));

What does scan do (vs reduce)?

scan is like reduce but emits the accumulated value on every emission, not just at the end — perfect for accumulating state over a stream (a running total, or building up a list from actions).

actions$.pipe(
  scan((state, action) => reducer(state, action), initial)
)

combineLatest vs withLatestFrom.

combineLatest emits when any source emits. withLatestFrom emits only when the source emits, pulling in the latest value of the others — use it when one stream is the trigger and the rest are just context.

saveClicks$.pipe(
  withLatestFrom(this.formValue$),
  switchMap(([_, form]) => this.api.save(form))
)

What is finalize used for?

finalize runs a callback when the observable completes, errors, or is unsubscribed — the RxJS equivalent of finally. Common use: hide a loading spinner regardless of success or failure.

this.loading = true;
req$.pipe(finalize(() => this.loading = false)).subscribe();

debounceTime vs throttleTime vs auditTime vs sampleTime.

  • debounceTime — emit after a quiet gap (last value).
  • throttleTime — emit the first value, then ignore for a window.
  • auditTime — ignore for a window, then emit the latest value.
  • sampleTime — emit the latest value on a fixed interval.
// search — wait until they stop typing
input$.pipe(debounceTime(300))

// scroll/resize — react immediately, then at most 1/100ms
scroll$.pipe(throttleTime(100))

// high-frequency -> render at most 1 per frame, with the LATEST value
mousemove$.pipe(auditTime(16))

// periodic snapshot of a live value
price$.pipe(sampleTime(1000))

What are higher-order observables and *All operators?

A higher-order observable emits other observables. Flattening operators subscribe to the inner ones: mergeAll (concurrent), concatAll (in order), switchAll (cancel previous). The *Map variants (mergeMap, concatMap…) are just map + *All.

source$.pipe(map(id => this.load(id)), concatAll());
// same as: source$.pipe(concatMap(id => this.load(id)))

What is takeUntilDestroyed?

(Angular 16+) An operator that auto-completes a subscription when the component/directive is destroyed — replacing the manual destroy$ Subject + takeUntil boilerplate. Call it in an injection context, or pass a DestroyRef.

data$.pipe(takeUntilDestroyed()).subscribe(...);

How do you write a custom operator?

A pipeable operator is just a function that takes a source observable and returns a new one — usually by composing existing operators with pipe. This makes reusable, named stream transformations.

const cleanSearch = () =>
  (src$) => src$.pipe(debounceTime(300), distinctUntilChanged());

input$.pipe(cleanSearch());

What are RxJS schedulers?

A scheduler controls when and on what context emissions happen — e.g. asyncScheduler (macrotask), asapScheduler (microtask), animationFrameScheduler (rAF). Used for fine timing control and for virtual time in tests (TestScheduler).

observeOn(animationFrameScheduler)

How do you convert an Observable to a Promise?

Use firstValueFrom(obs$) (resolves with the first emission) or lastValueFrom(obs$) (waits for completion). These replaced the deprecated .toPromise(). Handy when awaiting a one-shot stream in async/await code.

const user = await firstValueFrom(this.api.getUser(id));

Why does my HTTP call fire twice with two async pipes?

Observables are cold — each subscription re-runs the producer, so two async pipes on the same http.get() trigger two requests. Fix by multicasting with shareReplay(1), or subscribe once and reuse the value.

readonly user$ = this.http.get('/me').pipe(shareReplay(1));

What happens to a stream after it errors?

An error is a terminal event — the observable stops and emits nothing further, and the subscription is torn down. To keep a long-lived stream alive (like a search box), handle the error inside the inner observable with catchError so the outer stream never sees it.

// A Subject that errors is dead — permanently.
const s = new Subject<number>();
s.subscribe({ error: e => console.log('sub1', e) });
s.error('boom');
s.subscribe({ error: e => console.log('sub2', e) }); // fires IMMEDIATELY
s.next(1);                                           // ignored — nothing happens

How do Observables and Signals interoperate?

(Angular 16+) toSignal(obs$) subscribes and exposes the latest value as a signal (auto-unsubscribes); toObservable(sig) turns a signal into a stream. Use RxJS for async/events, signals for synchronous local state, and bridge between them.

count = toSignal(this.store.count$, { initialValue: 0 });

When should you use tap?

tap runs a side effect (logging, setting a flag, triggering an action) without altering the emitted value or the stream. Great for debugging and for effects that shouldn't change data. Never mutate values inside it.

req$.pipe(tap(() => this.loading = true), /* ... */);