20+ Angular RxJS Interview Questions 2025: Observables, Subjects & Operators

·15 min read
angularrxjsinterview-questionsobservablesreactive-programmingfrontend

RxJS has over 600 operators, yet Angular interviews consistently focus on the same 15-20 concepts. Developers use switchMap and mergeMap daily but often can't explain when to choose one over the other. They subscribe to Observables everywhere but don't know which ones need manual cleanup. This guide covers the RxJS patterns that matter in Angular interviews.

Table of Contents

  1. Observable Fundamentals Questions
  2. Hot vs Cold Observables Questions
  3. Subjects Questions
  4. Flattening Operators Questions
  5. Error Handling Questions
  6. Memory Leaks Questions
  7. Combining Streams Questions
  8. Quick Reference

Observable Fundamentals Questions

These questions test your understanding of Observables and how they differ from Promises.

What is RxJS and why does Angular use it?

RxJS is a library for reactive programming using Observables. Angular uses it because it provides a powerful way to handle asynchronous operations—HTTP requests, user events, timers—as streams of data that can be transformed, combined, and managed with operators.

RxJS represents async operations as Observable streams. Instead of callbacks or Promises, you subscribe to an Observable and receive values over time. The real power comes from operators—pure functions that transform these streams. For example, map transforms values, filter removes unwanted ones, switchMap handles nested async operations, and catchError handles failures.

In Angular specifically, HttpClient returns Observables, which means you can cancel requests mid-flight, automatically retry failed ones, and transform responses in a pipeline. The async pipe in templates automatically subscribes when the component renders and unsubscribes when it's destroyed, preventing memory leaks without manual cleanup.

What is the difference between an Observable and a Promise?

The fundamental difference is that Promises execute immediately when created, while Observables are lazy and only execute when subscribed to. This has significant practical implications.

// Promises execute immediately
const promise = new Promise((resolve) => {
    console.log('Promise executing'); // Runs RIGHT NOW
    resolve('done');
});
// The console.log already fired, even though we haven't used the result
 
// Observables wait until someone subscribes
const observable = new Observable((subscriber) => {
    console.log('Observable executing'); // Doesn't run yet
    subscriber.next('value 1');
    subscriber.next('value 2');
    subscriber.complete();
});
// Nothing has happened yet - the Observable is just waiting
 
// Now it executes:
const subscription = observable.subscribe((value) => console.log(value));
 
// And we can cancel it:
subscription.unsubscribe();

The practical implications are significant. Imagine a user rapidly typing in a search box. With Promises, every keystroke fires a request that can't be cancelled—you end up with race conditions where results arrive out of order. With Observables combined with switchMap, each new keystroke cancels the previous request, and you only get the result for the final search term.

FeaturePromiseObservable
ExecutionEager - runs immediately when createdLazy - only runs when subscribed
ValuesResolves once with a single valueCan emit many values over time
CancellationImpossible once startedCall unsubscribe() anytime
OperatorsLimited to .then() chainingHundreds of operators for transformation
Error handling.catch() at the endcatchError, retry, retryWhen in the pipe
Angular usageRarely preferredStandard for HttpClient, Forms, Router

How do you convert between Observables and Promises?

Sometimes you need to convert between them when integrating with Promise-based libraries or when you genuinely only need a single value.

// Observable to Promise - when you truly need just one value
const result = await firstValueFrom(this.http.get('/api/data'));
 
// Promise to Observable - when wrapping legacy Promise-based code
const observable = from(fetch('/api/data'));

Use firstValueFrom (or lastValueFrom) to convert Observables to Promises. Use from() to wrap Promises as Observables.


Hot vs Cold Observables Questions

These questions test your understanding of Observable execution and data sharing.

What is the difference between hot and cold Observables?

A cold Observable creates a new data producer for each subscriber—data is created inside the Observable. Each subscription triggers independent execution. Think of it like a Netflix movie where each viewer gets their own independent playback from the beginning.

// Cold Observable - like Netflix
const cold$ = new Observable((subscriber) => {
    console.log('Starting new playback');
    subscriber.next(Math.random()); // Each subscriber gets different random number
    subscriber.complete();
});
 
cold$.subscribe((v) => console.log('Viewer 1:', v)); // 0.123
cold$.subscribe((v) => console.log('Viewer 2:', v)); // 0.789 - different!

A hot Observable shares a single data producer among subscribers—data is created outside the Observable. All subscribers receive the same values, like a live TV broadcast where all viewers see the same thing at the same time.

// Hot Observable - like live TV
const subject = new Subject<number>();
 
subject.subscribe((v) => console.log('Viewer 1:', v));
subject.subscribe((v) => console.log('Viewer 2:', v));
 
subject.next(1); // Both viewers see 1
subject.next(2); // Both viewers see 2

Why does HttpClient being cold matter in Angular?

Angular's HttpClient returns cold Observables, which means each subscription triggers a new network request. This can cause unintended duplicate requests.

const data$ = this.http.get('/api/users');
data$.subscribe(); // Makes HTTP request #1
data$.subscribe(); // Makes HTTP request #2 - probably not what you wanted!

To share results across multiple subscribers, use shareReplay:

@Injectable({ providedIn: 'root' })
export class UserService {
    // Without shareReplay: every component that subscribes makes a new HTTP request
    // With shareReplay(1): one request is made, and the result is shared
    private users$ = this.http.get<User[]>('/api/users').pipe(
        shareReplay(1)
    );
 
    getUsers(): Observable<User[]> {
        return this.users$;
    }
}

The 1 in shareReplay(1) means "keep the last 1 value and replay it to new subscribers." The first subscriber triggers the HTTP request, and subsequent subscribers immediately receive the cached result.


Subjects Questions

These questions test your understanding of manually pushing values into streams.

What is a Subject in RxJS?

A Subject is a special type that's both an Observable and an Observer—it can both produce and consume values. Regular Observables are "pull-based" and produce values when subscribed to. Subjects let you manually push values into a stream, which is useful when a user clicks a button or when data arrives from a WebSocket.

RxJS provides four Subject types, each designed for different scenarios.

What is the difference between Subject, BehaviorSubject, ReplaySubject, and AsyncSubject?

Subject has no memory—if you emit a value before anyone subscribes, that value is lost forever:

const subject = new Subject<string>();
 
subject.next('A'); // Lost - no one is listening yet
 
subject.subscribe((v) => console.log(v)); // Subscriber never sees 'A'
 
subject.next('B'); // Subscriber sees 'B'
subject.next('C'); // Subscriber sees 'C'

BehaviorSubject requires an initial value and always maintains a "current" value that new subscribers receive immediately:

const currentUser = new BehaviorSubject<string>('guest');
 
currentUser.subscribe((v) => console.log('Sub 1:', v)); // Immediately gets 'guest'
 
currentUser.next('alice');
 
currentUser.subscribe((v) => console.log('Sub 2:', v)); // Immediately gets 'alice'
 
// Synchronous access to current value
console.log(currentUser.getValue()); // 'alice'

ReplaySubject remembers past values and replays them to new subscribers:

const chatHistory = new ReplaySubject<string>(3); // Remember last 3 values
 
chatHistory.next('Hello');
chatHistory.next('How are you?');
chatHistory.next('I am fine');
chatHistory.next('Thanks for asking');
 
// New subscriber gets last 3: 'How are you?', 'I am fine', 'Thanks for asking'
chatHistory.subscribe((msg) => console.log(msg));

AsyncSubject only emits its final value when it completes:

const calculation = new AsyncSubject<number>();
 
calculation.subscribe((v) => console.log(v));
 
calculation.next(1);   // Not emitted yet
calculation.next(2);   // Not emitted yet
calculation.next(42);  // Not emitted yet
 
calculation.complete(); // NOW it emits 42

When should you use BehaviorSubject in Angular services?

BehaviorSubject is ideal for state management in services because it provides a current value and replays to late subscribers. A common pattern exposes the Subject privately and provides a public Observable:

@Injectable({ providedIn: 'root' })
export class AuthService {
    // Private BehaviorSubject - only this service can emit values
    private currentUserSubject = new BehaviorSubject<User | null>(null);
 
    // Public Observable - components can subscribe but not emit
    currentUser$ = this.currentUserSubject.asObservable();
 
    login(user: User) {
        this.currentUserSubject.next(user);
    }
 
    logout() {
        this.currentUserSubject.next(null);
    }
 
    // Synchronous access when needed
    isLoggedIn(): boolean {
        return this.currentUserSubject.getValue() !== null;
    }
}

The asObservable() call prevents other parts of your application from accidentally calling next() on your Subject. Only the AuthService can change the current user.


Flattening Operators Questions

These questions test your ability to handle nested async operations—one of the most important RxJS concepts.

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

All four operators transform values from an outer Observable into inner Observables, but they differ in how they handle timing and concurrency.

switchMap cancels the previous inner Observable whenever a new value arrives. Use for search/autocomplete where only the latest result matters:

this.searchControl.valueChanges.pipe(
    debounceTime(300),
    distinctUntilChanged(),
    switchMap((term) => this.searchService.search(term))
    // Each new keystroke cancels the previous HTTP request
).subscribe((results) => this.results = results);

mergeMap runs all inner Observables concurrently without cancellation. Use for parallel operations like file uploads:

this.selectedFiles$.pipe(
    mergeMap((file) => this.uploadService.upload(file), 3) // Max 3 concurrent
).subscribe((result) => console.log('Uploaded:', result.filename));

concatMap queues each inner Observable and waits for it to complete before starting the next. Use when order matters:

this.saveActions$.pipe(
    concatMap((stepData) => this.api.saveStep(stepData))
    // Step 2 won't start until Step 1's HTTP response arrives
).subscribe();

exhaustMap ignores new values while the current inner Observable is still running. Use to prevent duplicate submissions:

this.submitClicks$.pipe(
    exhaustMap(() => this.api.submitForm(this.form.value))
    // Clicks while request is in-flight are ignored
).subscribe((response) => this.handleSuccess(response));

When should you use switchMap?

Use switchMap when you only care about the result of the latest operation and want to cancel previous ones. The two most common use cases are search autocomplete and loading data based on route parameters.

For search autocomplete, each keystroke should cancel the previous request:

this.searchControl.valueChanges.pipe(
    debounceTime(300),
    distinctUntilChanged(),
    switchMap((term) => this.searchService.search(term))
).subscribe((results) => this.results = results);

For route-based data loading, navigating to a new route should cancel the previous data fetch:

this.route.params.pipe(
    switchMap((params) => this.userService.getUser(params['id']))
).subscribe((user) => this.user = user);

When should you use exhaustMap?

Use exhaustMap when you want to ignore incoming values while processing the current one. The classic use case is form submission—if the user clicks "Submit" multiple times, you only want to process the first click until the request completes.

this.submitClicks$.pipe(
    exhaustMap(() => this.api.submitForm(this.form.value))
).subscribe((response) => this.handleSuccess(response));

This prevents duplicate form submissions without disabling the button or adding loading state logic.


Error Handling Questions

These questions test your ability to build robust applications that handle failures gracefully.

How do you handle errors in RxJS?

The catchError operator intercepts errors and lets you decide what to do. You have three main options: return a fallback value, complete the stream gracefully, or rethrow the error.

this.http.get('/api/data').pipe(
    catchError((error) => {
        console.error('API Error:', error);
 
        // Option 1: Return a fallback value so the app continues working
        return of({ data: [], error: true });
 
        // Option 2: Return EMPTY to complete the stream gracefully
        // return EMPTY;
 
        // Option 3: Rethrow if you want calling code to handle it
        // return throwError(() => new Error('Failed to load data'));
    })
).subscribe({
    next: (data) => this.displayData(data),
    error: (err) => this.showError(err) // Only runs if you rethrow
});

In most Angular applications, returning a fallback value that the UI can handle gracefully is preferred. Nobody likes seeing a blank screen because one API call failed.

How do you implement retry logic in RxJS?

Use the retry operator for simple retry logic, or retryWhen for more sophisticated strategies like exponential backoff.

Simple retry:

this.http.get('/api/data').pipe(
    retry(3), // Try up to 3 more times before giving up
    catchError((error) => {
        return of({ error: 'Service temporarily unavailable' });
    })
).subscribe();

Exponential backoff for production applications:

this.http.get('/api/data').pipe(
    retryWhen((errors) =>
        errors.pipe(
            scan((retryCount, error) => {
                if (retryCount >= 3) throw error;
                return retryCount + 1;
            }, 0),
            delayWhen((retryCount) =>
                timer(Math.pow(2, retryCount) * 1000) // 1s, 2s, 4s delays
            )
        )
    ),
    catchError((error) => of({ error: 'Service unavailable after retries' }))
).subscribe();

What is the finalize operator used for?

The finalize operator runs whether the Observable succeeds, errors, or is unsubscribed. It's perfect for cleanup logic like hiding loading spinners:

this.loading = true;
 
this.http.get('/api/data').pipe(
    finalize(() => {
        this.loading = false; // Always runs, no matter what
    })
).subscribe({
    next: (data) => this.data = data,
    error: (err) => this.showError(err)
});

Memory Leaks Questions

These questions test your understanding of subscription management—one of the most common sources of bugs in Angular applications.

When do you need to manually unsubscribe in Angular?

Some Observables never complete on their own and require manual cleanup when your component is destroyed:

  • Infinite streams like interval() and timer() keep ticking forever
  • DOM events from fromEvent() keep listening forever
  • Subjects you create manually keep waiting for values
  • NgRx Store selects stay alive for the application lifetime

Other Observables handle cleanup automatically:

  • HttpClient requests complete after emitting one value
  • async pipe subscribes when rendering and unsubscribes when destroyed
  • Router events are managed by Angular
  • Finite streams like of() and from() complete immediately

What is the takeUntil pattern?

The takeUntil pattern uses a Subject that emits when the component is destroyed to automatically complete all subscriptions:

@Component({
    selector: 'app-dashboard',
    template: `<div>{{ lastUpdate }}</div>`
})
export class DashboardComponent implements OnInit, OnDestroy {
    lastUpdate: string = '';
    private destroy$ = new Subject<void>();
 
    ngOnInit() {
        interval(1000).pipe(
            takeUntil(this.destroy$)
        ).subscribe(() => {
            this.lastUpdate = new Date().toISOString();
        });
 
        fromEvent(window, 'resize').pipe(
            takeUntil(this.destroy$)
        ).subscribe(() => {
            this.handleResize();
        });
    }
 
    ngOnDestroy() {
        this.destroy$.next();
        this.destroy$.complete();
    }
}

When ngOnDestroy runs, it emits a value through destroy$, which causes all the takeUntil operators to complete their streams.

How does the async pipe prevent memory leaks?

The async pipe automatically subscribes when the template renders and unsubscribes when the component is destroyed. It's the safest way to handle subscriptions in templates:

@Component({
    selector: 'app-user-list',
    template: `
        <div *ngIf="users$ | async as users; else loading">
            <div *ngFor="let user of users">{{ user.name }}</div>
        </div>
        <ng-template #loading>Loading...</ng-template>
    `
})
export class UserListComponent {
    users$ = this.userService.getUsers();
 
    constructor(private userService: UserService) {}
    // No ngOnDestroy needed!
}

The async pipe also calls markForCheck() for you when using OnPush change detection.

How do you identify a memory leak in an Angular component?

A common interview scenario presents buggy code and asks you to find the problem:

// What's wrong with this component?
@Component({
    selector: 'app-clock',
    template: `{{ time }}`
})
export class ClockComponent implements OnInit {
    time: string = '';
 
    ngOnInit() {
        interval(1000).subscribe(() => {
            this.time = new Date().toLocaleTimeString();
        });
    }
}

The problem is that interval(1000) never completes. Every time this component is created, a new interval starts. When the component is destroyed, the interval keeps running. Navigate to this page ten times, and you have ten intervals running simultaneously.

The fix:

@Component({
    selector: 'app-clock',
    template: `{{ time }}`
})
export class ClockComponent implements OnInit, OnDestroy {
    time: string = '';
    private destroy$ = new Subject<void>();
 
    ngOnInit() {
        interval(1000).pipe(
            takeUntil(this.destroy$)
        ).subscribe(() => {
            this.time = new Date().toLocaleTimeString();
        });
    }
 
    ngOnDestroy() {
        this.destroy$.next();
        this.destroy$.complete();
    }
}

Combining Streams Questions

These questions test your ability to coordinate multiple data sources.

What is combineLatest and when should you use it?

combineLatest waits until all source Observables have emitted at least once, then emits an array of the latest values whenever any source emits. It's perfect for dashboards that need to react to changes in multiple data sources:

const user$ = this.userService.currentUser$;
const settings$ = this.settingsService.settings$;
const notifications$ = this.notificationService.unreadNotifications$;
 
combineLatest([user$, settings$, notifications$]).pipe(
    map(([user, settings, notifications]) => ({
        displayName: user.name,
        theme: settings.theme,
        unreadCount: notifications.length
    }))
).subscribe((dashboard) => {
    this.updateDashboard(dashboard);
});

Whenever the user changes, settings change, or new notifications arrive, the dashboard updates.

What is forkJoin and how does it differ from combineLatest?

forkJoin is like Promise.all()—it waits for all source Observables to complete, then emits a single array of their final values. Use it for loading multiple resources in parallel before proceeding:

forkJoin({
    users: this.http.get<User[]>('/api/users'),
    categories: this.http.get<Category[]>('/api/categories'),
    settings: this.http.get<Settings>('/api/settings')
}).subscribe(({ users, categories, settings }) => {
    this.initializePage(users, categories, settings);
});

Important caveat: if any source Observable errors, the entire forkJoin errors. And if any source never completes (like a BehaviorSubject that keeps emitting), forkJoin will never emit.

What is merge and when should you use it?

merge combines multiple Observables into one, emitting values from all sources as they arrive. Use it to handle user interactions from multiple input methods:

const clicks$ = fromEvent(button, 'click');
const keyPresses$ = fromEvent(document, 'keypress').pipe(
    filter((e: KeyboardEvent) => e.key === 'Enter')
);
const touches$ = fromEvent(button, 'touchend');
 
merge(clicks$, keyPresses$, touches$).subscribe(() => {
    this.submitForm();
});

The form submits whether the user clicks, presses Enter, or taps on mobile.


Quick Reference

ConceptKey Point
Observable vs PromiseObservables are lazy, can emit multiple values, and support cancellation
Cold ObservableCreates new producer per subscriber (HttpClient)
Hot ObservableShares producer among subscribers (Subjects, DOM events)
switchMapCancels previous - use for search/autocomplete
mergeMapRuns all concurrently - use for parallel operations
concatMapRuns sequentially - use when order matters
exhaustMapIgnores while busy - prevents double-click
BehaviorSubjectHas current value, replays to late subscribers
ReplaySubjectReplays last N values to late subscribers
takeUntilStandard pattern for subscription cleanup
async pipeBest choice for templates - auto-manages subscriptions
catchErrorHandle errors without breaking the stream
shareReplayCache and share HTTP responses
combineLatestEmit when any source emits (after all emit once)
forkJoinEmit once when all sources complete

Ready to ace your interview?

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

View PDF Guides