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.

Both handle async work, but they behave very differently:

  • Promise — a single future value; eager (runs the moment it's created); resolves/rejects once; can't be cancelled; no operators.
  • Observable — a stream of zero, one, or many values over time; lazy (runs only when you subscribe); cancellable via unsubscribe(); and comes with rich operators (map, filter, switchMap).

Angular's HttpClient returns Observables, which is why they're everywhere in Angular.

// 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 flattening operators — they take an Observable that maps each value to another Observable and flatten the inner ones into a single stream. They differ in how they treat a new inner while one is still running:

  • switchMapcancels the previous inner and switches to the latest. Best for search / autocomplete / route changes.
  • mergeMap — runs all inners concurrently, no cancelling. Best for independent parallel requests.
  • concatMapqueues inners and runs them one after another, in order. Best for sequential writes/saves.
  • exhaustMapignores new values while one is running. Best for preventing double submits (login, payment).
// switchMap - Search Box
this.searchControl.valueChanges.pipe(
  debounceTime(300),
  distinctUntilChanged(),
  switchMap(term => this.userService.search(term))
).subscribe();

Subject vs BehaviorSubject vs ReplaySubject vs AsyncSubject.

A Subject is both an Observable and an Observer — it multicasts one execution to many subscribers. The four variants differ in what a new subscriber receives:

  • Subject — no initial value; subscribers only get values emitted after they subscribe.
  • BehaviorSubject — needs an initial value and stores the latest; new subscribers immediately get the current value. Ideal for state.
  • ReplaySubject — replays the last N values to new subscribers.
  • AsyncSubject — emits only the final value, and only once the stream 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?

A subscription that never completes keeps running after the component is gone — leaking memory and firing duplicate work. Clean it up:

  • async pipe in the template — subscribes and unsubscribes automatically. The preferred option.
  • takeUntilDestroyed() (Angular 16+) — auto-completes on destroy, no boilerplate.
  • takeUntil(destroy$) with a Subject completed in ngOnDestroy.

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

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

Hot vs cold observables.

  • Cold — each subscriber starts its own execution, so everyone gets the values from the beginning (e.g. an HTTP call re-runs on every subscribe).
  • Hot — the source is shared and already running, so late subscribers miss earlier values (e.g. DOM events, a Subject).

Turn a cold stream hot/shared with share() or shareReplay().

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

Name common RxJS operators and what they do.

Operators are functions you chain inside pipe() to transform, filter, combine, or control a stream. The everyday kit:

  • Transformmap, tap, scan.
  • Filterfilter, take, distinctUntilChanged.
  • Rate-limitdebounceTime, throttleTime.
  • FlattenswitchMap, mergeMap, concatMap.
  • CombinecombineLatest, forkJoin, merge.
  • ErrorscatchError, retry, finalize.
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?

  • catchError — intercept the error and return a fallback Observable (or rethrow).
  • retry(n) / retry({ delay }) — re-subscribe on failure.

Key point: an error terminates the stream — nothing emits after it. So in a long-lived pipeline (like a search box), put catchError inside the inner Observable (within switchMap) so one failed request doesn't kill the whole stream.

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

combineLatest vs forkJoin vs merge vs zip.

Four ways to combine streams, differing on when they emit:

  • combineLatest — emits the latest of each source whenever any source emits (after all have emitted once).
  • forkJoin — waits for all sources to complete, then emits their last values once — like Promise.all.
  • merge — interleaves values from all sources as they arrive.
  • zip — pairs values by index (1st with 1st, 2nd with 2nd).
// 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 as-is, then completes.
  • from(source) — converts an array, iterable, or Promise into a stream.
  • fromEvent(el, 'click') — a stream of DOM/Node 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 the method that chains operators together. Each operator takes the output of the previous one and returns a new Observable — the original stream is never modified.

Example: source$.pipe(filter(...), map(...), debounceTime(300)). This composition is how you build readable reactive pipelines.

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?

In a search box you pair these two:

  • debounceTime(300) — waits until the user pauses typing, so you don't fire on every keystroke.
  • distinctUntilChanged — drops a value equal to the previous one (e.g. type then delete back to the same term), avoiding a duplicate request.

What does startWith do?

startWith(value) emits one or more values immediately on subscribe, before the source starts.

It's commonly used to give the UI an initial/loading state to render while the real data is still on its way, and pairs well with combineLatest so it doesn't have to wait on every source.

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.

The canonical type-ahead pipeline chains four operators off the input's valueChanges:

  • debounceTime(300) — wait for the user to pause.
  • distinctUntilChanged — skip duplicate terms.
  • switchMap(term => search(term)) — cancel the previous request so only the latest result wins.
  • catchError inside the switchMap — so one failed search 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?

Poll on a timer and swap to the request each tick:

  • timer(0, N) — emit immediately, then every N ms (or interval(N)).
  • switchMap(() => apiCall()) — so a slow response doesn't overlap the next tick.
  • takeUntilDestroyed() / takeUntil — stop polling when the component is destroyed.
timer(0, 5000).pipe(
  switchMap(() => this.api.getStatus()),
  takeUntil(this.destroy$)
)

How do you retry a failed request with exponential backoff?

Retry a failed request with a growing delay so you don't hammer a struggling server:

  • retry({ count, delay }) (RxJS 7.4+) — the delay function returns an increasing wait per attempt (e.g. 1s, 2s, 4s).
  • In older versions, use retryWhen with an expanding timer.
  • Cap the retry count, and finish with catchError for a final fallback.
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 HTTP call runs once, and every current and future subscriber receives the last emitted value — a one-line in-memory cache.

The 1 is the buffer size (replay the most recent value). Without it, each subscriber re-runs the cold HTTP call. Add a TTL / invalidation if the data can go stale.

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

What does scan do (vs reduce)?

scan is like reduce, but it emits the accumulated value on every emission instead of only at the end.

  • scan — a running accumulation over time (a live total, or building a list from actions).
  • reduce — emits a single final value, only when the stream completes.
actions$.pipe(
  scan((state, action) => reducer(state, action), initial)
)

combineLatest vs withLatestFrom.

  • combineLatest — emits whenever any source emits.
  • withLatestFrom — emits only when the primary source emits, pulling in the latest value of the others.

Use withLatestFrom when one stream is the trigger and the rest are just context — e.g. a click combined with the current form state.

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

What is finalize used for?

finalize(fn) runs a callback when the stream completes, errors, or is unsubscribed — the RxJS equivalent of finally.

The classic use is hiding a loading spinner regardless of whether the request succeeded or failed.

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

debounceTime vs throttleTime vs auditTime vs sampleTime.

All four limit how often a stream emits, but differ on which emission they keep:

  • debounceTime — emit after a quiet gap (the last value once typing stops).
  • throttleTime — emit the first value, then ignore for the window (leading).
  • auditTime — ignore for the window, then emit the latest value (trailing).
  • sampleTime — emit the latest value on a fixed clock.
// 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 is an Observable that emits other Observables. You flatten it with:

  • mergeAll — subscribe to all inners concurrently.
  • concatAll — one inner at a time, in order.
  • switchAll — cancel the previous inner for the latest.

The *Map operators (mergeMap, concatMap, switchMap) are just map + the matching *All in one step — which is what you normally use.

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

What is takeUntilDestroyed?

takeUntilDestroyed() (Angular 16+) is an operator that auto-completes a subscription when the component/directive is destroyed.

It replaces the old boilerplate of a manual destroy$ Subject plus takeUntil. Call it in an injection context, or pass a DestroyRef to use it elsewhere.

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

How do you write a custom operator?

A pipeable operator is simply a function that takes a source Observable and returns a new one — usually by composing existing operators with pipe.

This lets you package a repeated pipeline into one reusable, named, testable operator you can drop into any pipe().

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

input$.pipe(cleanSearch());

What are RxJS schedulers?

A scheduler controls when and on what context a stream emits:

  • asyncScheduler — via setTimeout (a macrotask).
  • asapScheduler — a microtask.
  • animationFrameScheduler — on requestAnimationFrame.

Most apps never set one explicitly; the main uses are fine-grained timing and virtual time in tests (TestScheduler).

observeOn(animationFrameScheduler)

How do you convert an Observable to a Promise?

Convert an Observable to a Promise so you can await it:

  • firstValueFrom(obs$) — resolves with the first emission, then unsubscribes.
  • lastValueFrom(obs$) — resolves with the last value, on completion.

These replaced the deprecated .toPromise().

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

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

Observables are cold — every subscription re-runs the producer. Two async pipes on the same http.get() are two subscriptions, so the request fires twice.

Fix it by multicasting one execution with shareReplay(1), or subscribe once and reuse the value (a view-model or signal).

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, emits nothing further, and the subscription is torn down (just like complete).

To keep a long-lived stream (like a search box) alive, handle the error inside the inner Observable with catchError, so the outer stream never sees the error.

// 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+ bridges RxJS and Signals in both directions:

  • toSignal(obs$) — subscribes and exposes the latest value as a signal (and auto-unsubscribes on destroy).
  • toObservable(sig) — turns a signal back into a stream.

Use RxJS for async/events and signals for synchronous local state, converting at the boundary.

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 changing the emitted value or the stream.

It's ideal for debugging and incidental effects. Keep real data transformations in map/switchMap — don't hide business logic in tap, and never mutate values inside it.

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