21 Angular RxJS Interview Questions: Streams & Operators

·13 min read
By ·Updated
angularrxjsinterview-questionsobservablesreactive-programmingfrontend

RxJS interview questions are less about memorizing operators and more about defining the stream contract: when work starts, how many inner operations may run, what unsubscription tears down, where errors are recovered, and who owns sharing and cleanup.

This guide contains exactly 21 questions and uses Angular's current RxJS interop. It avoids treating every Observable as lazy or every unsubscription as cancellation of a server-side effect.

Table of Contents

  1. Observable fundamentals
  2. Cold, hot and shared execution
  3. Subjects and state
  4. Flattening operators
  5. Errors, retry and cleanup
  6. Combining RxJS with Angular Signals
  7. Frequently asked questions

Observable fundamentals

1. What is RxJS and why does Angular use it?

RxJS models values, errors and completion over time through Observables and provides operators for transforming and coordinating streams. Angular APIs such as HttpClient, router events and Reactive Forms expose Observables because cancellation/teardown, multiple emissions and composition are useful at those boundaries.

RxJS is not mandatory for every piece of state. Angular Signals are often simpler for a current synchronous value used by templates; RxJS excels at temporal operations such as debounce, concurrency, cancellation, retries and combining event sources.

2. What is the difference between an Observable and a Promise?

A Promise settles once. The executor passed to new Promise() runs synchronously during construction, although a Promise-returning factory can defer creating it. Promise consumers observe fulfillment or rejection.

An Observable defines what happens on subscription and may emit zero, one or many next values followed by complete or error. A subscription may expose teardown through unsubscribe().

const values$ = new Observable<number>((subscriber) => {
  const timer = setInterval(() => subscriber.next(Date.now()), 1000);
  return () => clearInterval(timer);
});
 
const subscription = values$.subscribe(console.log);
subscription.unsubscribe(); // Runs this producer's teardown.

Many Observable factories are lazy/cold, but an Observable can wrap a producer that already exists. Do not define the type solely as “lazy Promise with many values.”

3. Does unsubscribe cancel the underlying work?

Unsubscription stops delivery to that subscription and runs the producer/operator teardown. What that does depends on the source. It may remove a DOM listener, clear a timer or abort an Angular HttpClient request. A badly written source may ignore teardown.

Even an aborted HTTP client request may already have reached the server. Unsubscription cannot reliably undo a payment, email or database write. Mutating APIs still need server-side idempotency, authorization and recovery for unknown outcomes.

4. How do firstValueFrom and lastValueFrom differ?

firstValueFrom(source$) resolves on the first emission and unsubscribes. lastValueFrom(source$) waits for completion and resolves with the final emitted value. Both reject if the source errors; both can reject with EmptyError when no value exists unless configured with a default.

Use take(1), timeout or another completion bound when the source might neither emit nor complete. Otherwise the awaited Promise can remain pending indefinitely. Conversion also discards the rest of the stream contract, so do it only at a real Promise boundary.


Cold, hot and shared execution

5. What do cold and hot mean in RxJS?

A cold source creates or owns an execution per subscription. A hot source produces independently of a particular subscriber, so late subscribers miss earlier values unless replay is added. Multicasting can turn a cold execution into shared execution for a group of subscribers.

These are source/execution properties, not guarantees of the Observable class. Ask who creates the producer, whether subscriptions share it, what late subscribers receive and when it is torn down.

6. Why does HttpClient being cold matter?

Angular HttpClient sends a request only after subscription, and each subscription to the same returned Observable sends an independent backend request:

const user$ = this.http.get<User>('/api/users/u1');
 
user$.subscribe(); // request 1
user$.subscribe(); // request 2

Unsubscribing aborts an in-flight request on the client. Responses usually complete, though interceptors can alter behavior. Duplicate subscription can be correct for a refresh; otherwise assign one owner or share deliberately.

7. Is shareReplay a complete HTTP cache?

No. This operator can share a source subscription and replay buffered notifications, but the configuration and lifecycle matter:

readonly user$ = this.http.get<User>('/api/users/u1').pipe(
  shareReplay({ bufferSize: 1, refCount: true }),
);

refCount: true disconnects while the source is active when the last subscriber leaves; completion/error/reset behavior still needs review for the installed RxJS version. More importantly, shareReplay has no knowledge of user/tenant scope, TTL, mutation invalidation, refresh, authorization changes or persistent cache. Model those policies explicitly.


Subjects and state

8. How do Subject, BehaviorSubject, ReplaySubject and AsyncSubject differ?

  • Subject multicasts future values and has no initial/current value.
  • BehaviorSubject requires an initial value and synchronously gives new subscribers its current value.
  • ReplaySubject buffers a configured number/time window of notifications for later subscribers; an unbounded buffer risks memory growth.
  • AsyncSubject emits its last value only when the source completes; if it never completes, subscribers receive no value.

All Subjects expose imperative next/error/complete, so keep the writable side private when consumers should only observe.

9. Should Angular service state use BehaviorSubject or a Signal?

Use a Signal when the domain is primarily current synchronous state read by Angular consumers. Use an Observable/Subject when temporal operator composition, multiple events, backpressure-like rate control or external Observable APIs dominate. Hybrid services may expose both via official interop.

BehaviorSubject.getValue() is synchronous but can throw after an error and makes imperative reads easy to scatter. A Subject is not automatically a state store: define update authority, invariants, loading/error representation and lifecycle regardless of primitive.


Flattening operators

10. What is the difference between switchMap, mergeMap, concatMap and exhaustMap?

OperatorNew source value while an inner is activeBest fit
switchMapUnsubscribes previous inner, subscribes latestLatest-result-wins reads
mergeMapRuns multiple inners; concurrency can be boundedIndependent work
concatMapQueues until the previous inner completesOrdered serial work
exhaustMapIgnores the new source valueIgnore re-entry while busy

If an inner never completes, concatMap can stall its queue and exhaustMap can ignore future input forever. With mergeMap, unbounded fast input can create excessive concurrency. Operator choice is a correctness decision before it is a style choice.

11. What does switchMap actually cancel?

It unsubscribes the previous inner Observable. For Angular HttpClient, that aborts the client request if it is still in progress. It does not guarantee that the server never received or executed it.

readonly results$ = this.query.valueChanges.pipe(
  debounceTime(250),
  distinctUntilChanged(),
  switchMap((query) => this.search.search(query)),
);

This is suitable for replaceable reads such as search. It is usually wrong for independent writes that must all complete; use an appropriate queue/concurrency model plus idempotent server operations.

12. Does exhaustMap prevent duplicate submissions?

It ignores source values while its current inner subscription is active. That can suppress rapid double-clicks in one client instance, but it does not cover a retry after completion, another tab/device, reconnect, proxy retry or duplicated message.

Use it as a UX/concurrency policy, not a durable uniqueness guarantee. The submit endpoint should enforce authorization, validation and idempotency with a caller-scoped key or domain constraint where duplicates matter.

13. How should mergeMap concurrency be controlled?

Pass a concurrency limit when each source value launches independent work:

readonly uploads$ = this.files$.pipe(
  mergeMap((file) => this.uploader.upload(file), 3),
);

Choose the bound from server quotas, browser connections, payload sizes, memory and desired responsiveness. Concurrency does not preserve completion order. If ordering is a business invariant, concatMap or a server-side sequencing design may be required.


Errors, retry and cleanup

14. Where should catchError be placed?

Placement determines which scope terminates. catchError inside a flattening operator can recover one inner request while the outer user-action stream continues; placing it outside can replace or terminate the whole chain.

Return a value only if it is semantically a valid fallback. EMPTY silently completes that scope and can hide a missing state transition. Rethrow with throwError(() => error) when the owner must decide. Errors may be mapped in a data service, orchestration layer or component boundary—“always services, never components” is not a sound universal rule.

15. How should HTTP retry be implemented?

Use bounded retry with a status/method-aware delay policy rather than deprecated retryWhen:

readonly config$ = this.http.get<Config>('/api/config').pipe(
  retry({
    count: 3,
    delay: (error, retryCount) => {
      if (!isTransient(error)) return throwError(() => error);
      return timer(Math.min(500 * 2 ** (retryCount - 1), 4000));
    },
  }),
);

Add jitter for coordinated clients and respect server signals such as Retry-After where applicable. Do not retry authentication/validation failures or non-idempotent writes blindly. Cap total time and keep cancellation available.

16. What does finalize guarantee?

finalize(callback) runs when that subscription ends through completion, error or unsubscription. It is appropriate for subscription-scoped cleanup, but repeated/retried/shared chains can make placement significant.

For a loading indicator, account for concurrent requests; one request finalizing should not hide another still in flight. An atomic counter, request identity or declarative state model is safer than a global Boolean toggled by every request.

17. When should an Angular component unsubscribe?

Clean up when a subscription can outlive its owner or its callback should not run after destruction. Long-lived timers, events, router/store streams and Subjects are obvious cases. Angular also recommends cleanup of HttpClient subscriptions even though they usually complete, because a late callback may touch a destroyed component and cleanup aborts an in-flight client request.

Prefer declarative ownership (AsyncPipe, toSignal) or takeUntilDestroyed. Do not maintain a memorized allowlist such as “Router events never need cleanup”; lifecycle depends on the subscriber and source contract.

18. How does takeUntilDestroyed replace the destroy Subject pattern?

The stable Angular operator binds teardown to a DestroyRef:

private readonly destroyRef = inject(DestroyRef);
 
start() {
  this.notifications.messages.pipe(
    takeUntilDestroyed(this.destroyRef),
  ).subscribe((message) => this.toast.show(message));
}

Inside an injection context such as a constructor, the argument can be omitted. Outside it, pass DestroyRef explicitly. The older destroy$ + takeUntil pattern still works but adds a Subject and an ngOnDestroy protocol that can be implemented incorrectly.

19. How do AsyncPipe and toSignal own subscriptions?

AsyncPipe subscribes for a rendered view, marks it for check on emissions and unsubscribes when the view is destroyed or the expression source changes. toSignal subscribes immediately, usually cleans up with the injection context, and exposes the latest value synchronously as a Signal.

Create toSignal once and reuse it; repeatedly calling it creates repeated subscriptions. Decide the initial value or use requireSync only when the source truly emits synchronously. Observable errors are thrown when the Signal is read unless the stream handles them.


Combining RxJS with Angular Signals

20. How do combineLatest and forkJoin differ?

combineLatest waits until every source has emitted at least once, then emits whenever any source changes. A source with no initial emission can prevent output; use an intentional initial state rather than blindly adding startWith.

forkJoin waits for every source to complete and emits their last values once. It never emits if an input never completes, and an unhandled input error errors the combination. It fits independent finite requests, while combineLatest fits ongoing derived state.

21. What timing caveat does toObservable have?

toObservable(signal) uses an Angular effect. It exposes current state to subscribers, but subsequent propagation is asynchronous; multiple signal writes before the effect runs can be coalesced so only the latest stabilized value is emitted.

readonly query = signal('');
readonly query$ = toObservable(this.query);
readonly results$ = this.query$.pipe(
  debounceTime(200),
  switchMap((value) => this.search.search(value)),
);

Do not use the conversion when every intermediate write is a business event that must be preserved. Model such events directly as a stream instead of deriving them from state snapshots.


Quick reference

NeedTypical operator/toolCritical caveat
Latest replaceable readswitchMapInner unsubscribe may not undo server work
Bounded parallel workmergeMap(..., concurrency)Completion order differs
Ordered serial workconcatMapNon-completing inner stalls queue
Ignore re-entryexhaustMapNot durable idempotency
Component cleanuptakeUntilDestroyedPass DestroyRef outside injection context
Template subscriptionAsyncPipeHigh-rate work still has cost
Current Observable value as statetoSignalImmediate subscription; define initial/error behavior
Share/replay executionshareReplayNot a business cache policy
Finite parallel completionforkJoinEvery input must complete

Frequently Asked Questions

What is the difference between an Observable and a Promise?

A Promise settles once and its executor runs when the Promise is constructed. An Observable may emit zero, one or many notifications and defines subscription and teardown behavior. Many Observables are lazy, but not all are cold. Unsubscribing stops the RxJS subscription only to the extent that the producer implements teardown; it does not universally undo external work.

What is the difference between switchMap, mergeMap, concatMap and exhaustMap?

switchMap unsubscribes the previous inner stream, mergeMap keeps multiple inners active with optional concurrency, concatMap queues inners and preserves subscription order, and exhaustMap ignores source values while one inner is active. Choose by cancellation, concurrency, ordering and drop semantics, then handle server-side idempotency separately.

When should an Angular component unsubscribe?

Clean up whenever a subscription may outlive the component or its callback should not run after destruction. Prefer AsyncPipe or toSignal for rendered state and takeUntilDestroyed for imperative subscriptions. Even though HttpClient usually completes, Angular recommends cleanup because a late callback can still touch a destroyed component and unsubscription can abort an in-flight client request.

Is shareReplay a complete HTTP cache?

No. shareReplay can share one subscription and replay buffered notifications, but it does not define business freshness, invalidation, authorization scope, mutation coherence or durable storage. Choose bufferSize, refCount and reset behavior deliberately, and put cache ownership, keys, expiry and refresh policy in a service or data layer.

How should HTTP requests be retried with RxJS?

Retry only failures and operations that are safe under the API contract, with a bounded count, delay or backoff, jitter where appropriate, and cancellation. Do not blindly retry non-idempotent mutations or deterministic 4xx responses. Current RxJS supports retry with a delay configuration; retryWhen is deprecated for future removal.

How do RxJS Observables interoperate with Angular Signals?

toSignal subscribes immediately, exposes the latest Observable value synchronously through a Signal and normally cleans up with its injection context. toObservable uses an effect and may coalesce multiple Signal writes before emitting the latest stabilized value. Pick Signals for current state and RxJS for stream composition rather than converting repeatedly without a semantic reason.


Sources

Ready to ace your interview?

Get 550+ interview questions with detailed answers in our comprehensive PDF guides.

View PDF Guides