What is RxJS?
RxJS is a library for working with values that arrive over time. Instead of asking "what is the value now", you say what should happen each time a new value arrives. It is the base of Angular HTTP, forms and routing.
38 questions - Intermediate
RxJS is where Angular interviews get hard. Interviewers usually ask four things: how an Observable is different from a Promise, the four flattening operators, the Subject types, and how you avoid memory leaks. Learn those four well and the rest follows.
Last updated:
RxJS is a library for working with values that arrive over time. Instead of asking "what is the value now", you say what should happen each time a new value arrives. It is the base of Angular HTTP, forms and routing.
An Observable is a plan for producing values. Nothing happens until someone subscribes. It can emit zero, one or many values, and then it either completes or errors. After complete or error, it never emits again.
const numbers$ = new Observable((subscriber) => {
subscriber.next(1);
subscriber.next(2);
subscriber.complete();
});
numbers$.subscribe({
next: (v) => console.log(v),
error: (e) => console.error(e),
complete: () => console.log("done"),
});
A Promise starts at once, gives exactly one value, and cannot be cancelled. An Observable is lazy, can give many values over time, can be cancelled by unsubscribing, and can be retried and changed with operators.
The function inside the Observable does not run until you call subscribe. So an HTTP Observable that nobody subscribes to never sends a request. This surprises many beginners.
An Observer is the object with next, error and complete that receives the values. A Subscription is what subscribe returns. Calling unsubscribe on it stops the stream and frees resources.
A cold Observable makes a new producer for every subscriber, so each one gets its own run. An HTTP call is cold, so two subscribers cause two requests. A hot Observable shares one producer, so every subscriber sees the same values. A Subject or a DOM event stream is hot.
Use a multicasting operator such as share or shareReplay. shareReplay also keeps the last values, so a late subscriber still gets them. That is the usual way to cache an HTTP result.
readonly config$ = this.http.get<Config>("/api/config").pipe(
shareReplay({ bufferSize: 1, refCount: true })
);
of emits the values you give and completes. from turns an array, promise or iterable into a stream. interval and timer emit on a schedule. fromEvent listens to DOM events. EMPTY completes at once, and throwError errors at once.
pipe joins operators together. Each operator takes the stream and returns a new stream. It never changes the original one. Reading a pipe from top to bottom tells you exactly what happens to each value.
An operator only describes a change. It builds a new Observable and runs nothing. Subscribing is what really starts the work.
A Subject is both an Observable and an Observer. You can call next on it to push values, and other code can subscribe to it. It is multicast, so every subscriber gets the same value.
const clicks$ = new Subject<string>();
clicks$.subscribe((v) => console.log("A", v));
clicks$.next("save"); // A save
A BehaviorSubject needs a starting value and always keeps the latest one. Any new subscriber gets the current value at once. It is the standard way to hold state inside a service.
private user$ = new BehaviorSubject<User | null>(null);
readonly currentUser$ = this.user$.asObservable();
setUser(u: User) { this.user$.next(u); }
A ReplaySubject keeps a chosen number of past values, or a time window, and replays them to every new subscriber. Use it when late subscribers need the history, not only the latest value.
An AsyncSubject emits only the last value, and only when the subject completes. It is rarely used in app code.
When a plain Observable is enough. If you expose a Subject publicly, any code can push values into your state from anywhere, and bugs become very hard to trace. Keep the Subject private and expose it with asObservable().
EventEmitter extends Subject, but it is meant only for @Output between a child and its parent. Using it as a general event bus inside services is not advised. Use a Subject there.
map changes each value into another plain value. switchMap changes each value into a new Observable, subscribes to it, and flattens the result. Use map for data. Use switchMap when each value starts another async job.
switchMap cancels the previous inner Observable when a new value arrives, so it fits search as you type. mergeMap runs all of them at the same time with no order, so it fits independent parallel work. concatMap queues them and runs one at a time in order, so it fits work where order matters. exhaustMap ignores new values while one is still running, so it fits a submit button and a double click cannot save twice.
// search box: cancel the old request
search$.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap((q) => this.api.search(q))
);
// save button: ignore extra clicks
save$.pipe(exhaustMap(() => this.api.save(form.value)));
Because it cancels the previous one. If the user clicks save twice quickly, the first save is stopped in the middle, and the data can end up half saved. Use concatMap or exhaustMap for writes.
debounceTime waits until the stream has been quiet for the given time and then emits the last value. It fits typing. throttleTime emits at once and then ignores values for a period. It fits scroll and resize.
It drops a value that is the same as the one before it, so nothing after it runs for no reason. For objects, pass a compare function, because the default compares references.
combineLatest emits when any source emits, using the latest of each, but only after every source has emitted once. forkJoin waits for all sources to complete and emits the final values once, like Promise.all. zip pairs values by position. withLatestFrom emits only when the main source emits, and takes the latest of the others.
merge subscribes to all sources at once and mixes the values together. concat subscribes to the next source only after the previous one completes, so the order is strict.
It emits a value at once, before the source starts. It is a neat way to show a loading state or a default value while the real data is on the way.
tap lets you do a side effect, like logging or setting a loading flag, without changing the values. If your tap is changing the data, you should be using map.
take(n) takes n values and then completes. first() takes one, and errors if the source completes without emitting. takeUntil completes the stream when another Observable emits. That is the classic unsubscribe pattern.
Both build up a result. scan emits the running result after every value, so you can show a live total. reduce emits only the final result, and only when the source completes.
clicks$.pipe(scan((count) => count + 1, 0)); // 1, 2, 3...
shareReplay shares one subscription and replays the last values to new subscribers. Without refCount: true, the subscription underneath stays alive forever, even after everyone unsubscribes. That leaks, above all with an interval source.
Use catchError inside the pipe. Return a fallback Observable such as of([]) to recover, or throw again with throwError so the caller handles it. Remember that an error ends the stream.
this.http.get<User[]>("/api/users").pipe(
catchError((err) => {
console.error(err);
return of([]);
})
);
Because an error ends the stream for good. This hurts most when catchError sits on the outer stream of a valueChanges pipeline: one failed request kills the whole search box. Put catchError on the inner Observable inside switchMap, so only that one request fails.
search$.pipe(
switchMap((q) => this.api.search(q).pipe(catchError(() => of([]))))
);
retry(n) subscribes again at once, up to n times. retryWhen, now usually written as retry with a delay config, lets you decide when to try again, for example with a growing wait, and when to give up.
finalize runs a function when the stream completes, errors or is unsubscribed. It is the right place to hide a loading spinner, because it fires in all three cases.
Subscribing and never unsubscribing from a stream that never completes: an interval, a Subject, a router event or a form event stream. The component is destroyed, but the callback keeps running and keeps the old component in memory.
Best: let the AsyncPipe do it in the template. In code: takeUntilDestroyed() in Angular, or a destroy$ Subject with takeUntil, or collect subscriptions in one Subscription object and unsubscribe in ngOnDestroy. Note that take(1) and streams that complete clean themselves up.
private destroy$ = new Subject<void>();
ngOnInit() {
this.service.data$.pipe(takeUntil(this.destroy$)).subscribe();
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
It makes nested callbacks that are hard to cancel, hard to read and easy to leak. Use a flattening operator like switchMap or concatMap instead.
A marble diagram is a simple text or picture that shows values on a time line, like "--a--b--|". It is used to explain operators and to write tests with the TestScheduler, where you check the exact timing of each value.
A scheduler controls when a subscription starts and when values are delivered: at once, on a microtask, on a timer, or on animation frames. Most code never sets one. Tests use the TestScheduler to make time predictable.
firstValueFrom or lastValueFrom turn an Observable into a Promise. from turns a Promise into an Observable. Avoid the old toPromise, which is removed in RxJS 8.
const user = await firstValueFrom(this.api.getUser(id));
CrackInterviewAI listens to the live interview and gives a structured answer on screen for coding, system design, HR and project questions. Download for Windows.