What is RxJS?
RxJS is a library for working with streams of values over time. Instead of asking "what is the value now", you describe what should happen every time a new value arrives. It is the backbone of Angular HTTP, forms and routing.
38 questions - Intermediate
RxJS is where Angular interviews get hard. Interviewers usually ask four things: how an Observable differs from a Promise, the difference between the four flattening operators, Subject types, and how you avoid memory leaks. Learn those properly and you will handle the rest.
RxJS is a library for working with streams of values over time. Instead of asking "what is the value now", you describe what should happen every time a new value arrives. It is the backbone 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, then either complete or error - and 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 runs immediately, 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 transformed with operators.
The function inside the Observable does not run until subscribe is called. So an HTTP Observable that nobody subscribes to never sends a request - a very common surprise for 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 creates a new producer for every subscriber, so each one gets its own independent run - an HTTP call is cold and two subscribers cause two requests. A hot Observable shares one producer, so all subscribers see the same values - a Subject or a DOM event stream is hot.
With a multicasting operator such as share or shareReplay. shareReplay also stores the last values so a late subscriber still receives them, which 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 immediately, and throwError errors immediately.
pipe chains operators together. Each operator takes the stream, returns a new stream, and does not modify the original one. Reading a pipe from top to bottom tells you exactly what happens to each value.
An operator only describes a transformation - it builds a new Observable and runs nothing. Subscribing is what actually 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 Subject that needs a starting value and always remembers the latest one, so any new subscriber immediately receives the current value. It is the standard way to hold state in a service.
private user$ = new BehaviorSubject<User | null>(null);
readonly currentUser$ = this.user$.asObservable();
setUser(u: User) { this.user$.next(u); }
It remembers a chosen number of past values (or a time window) and replays them to every new subscriber. Use it when late subscribers need history, not just the latest value.
It emits only the final value, and only when the subject completes. It is rarely used in application code.
When a plain Observable would do. Exposing a Subject publicly lets any code push values into your state from anywhere, which makes bugs hard to trace. Keep the Subject private and expose it with asObservable().
EventEmitter extends Subject but is meant only for @Output bindings between a child and its parent. Using it as a general event bus inside services is discouraged; use a Subject there.
map transforms each value into another plain value. switchMap transforms each value into a new Observable and subscribes to it, flattening the result. Use map for data, switchMap when each value starts another async job.
switchMap cancels the previous inner Observable when a new value arrives - best for search-as-you-type. mergeMap runs all of them at once with no order guarantee - best for independent parallel work. concatMap queues them and runs one at a time in order - best when order matters. exhaustMap ignores new values while one is still running - best for a submit button, so a double click does not 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 aborted mid-flight and you may end up with half-saved data. Use concatMap or exhaustMap for writes.
debounceTime waits until the stream has been quiet for the given time and then emits the last value - good for typing. throttleTime emits immediately and then ignores values for a period - good for scroll and resize.
It drops a value that is the same as the previous one, so nothing downstream runs again for no reason. For objects, pass a compare function because the default is reference comparison.
combineLatest emits whenever any source emits, using the latest of each, but only after every source has emitted at least once. forkJoin waits for all sources to complete and emits the final values once - like Promise.all. zip pairs values by index. withLatestFrom emits only when the main source emits, and picks up the latest of the others.
merge subscribes to all sources at once and interleaves the values. concat subscribes to the next source only after the previous one has completed, so the order is strict.
It emits a value immediately before the source starts. It is a neat way to show a loading state or a default value while waiting for real data.
It lets you do a side effect - logging, setting a loading flag - without changing the values. If tap is changing your data, you should be using map.
take(n) takes n values then completes. first() takes one and errors if the source completes without emitting. takeUntil completes the stream when another Observable emits, which is the classic unsubscribe pattern.
Both accumulate values. scan emits the running result after every value, so you can show live totals. reduce emits only the final result when the source completes.
clicks$.pipe(scan((count) => count + 1, 0)); // 1, 2, 3...
shareReplay shares one subscription and replays the last values to newcomers. Without refCount: true, the underlying subscription stays alive forever even after everyone unsubscribes, which leaks - especially with an interval source.
With catchError inside the pipe. Return a fallback Observable such as of([]) to recover, or rethrow with throwError to let the caller deal with it. Remember that an error terminates the stream.
this.http.get<User[]>("/api/users").pipe(
catchError((err) => {
console.error(err);
return of([]);
})
);
Because an error is a terminal event. This bites hardest when catchError is placed 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 request fails.
search$.pipe(
switchMap((q) => this.api.search(q).pipe(catchError(() => of([]))))
);
retry(n) resubscribes immediately up to n times. retryWhen (now usually written as retry with a delay config) lets you decide when to retry, for example with an increasing backoff, and to give up after a limit.
It runs a function when the stream completes, errors or is unsubscribed. It is the right place to turn off a loading spinner, because it fires in all three cases.
Subscribing without ever unsubscribing to a stream that never completes - an interval, a Subject, a router or form event stream. The component is destroyed but the callback keeps running and holding 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. Also 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 creates nested callbacks that are hard to cancel, hard to read and easy to leak. Use a flattening operator like switchMap or concatMap instead.
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 assert the exact timing of emissions.
It controls when a subscription starts and when values are delivered - synchronously, on a microtask, on a timer, or on animation frames. Most code never sets one, but tests use the TestScheduler to make time deterministic.
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.