Angular RxJS Interview Questions: The Complete Guide to Reactive Programming

·23 min read
angularrxjsinterview-questionsobservablesreactive-programming

RxJS has over 600 operators, yet Angular interviews consistently focus on the same 10-15 concepts. Here's the paradox: developers use switchMap and mergeMap daily but can't explain when to choose one over the other. They subscribe to Observables everywhere but don't know which ones need manual cleanup.

The gap between using RxJS and understanding it is exactly what Angular interviewers probe for. This guide covers the patterns that matter.

Starting Simple: What Is RxJS and Why Should You Care?

Let's begin with the question you're almost guaranteed to hear. When an interviewer asks "What is RxJS and why does Angular use it?", resist the temptation to dive into technical details. Start with the simple answer:

"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. It enables features like automatic request cancellation, retry logic, debouncing, and complex async coordination that would be verbose with Promises."

That's your 30-second answer. Wait for follow-up questions rather than overwhelming them with everything you know. If they want more depth, you can expand on specific areas.

If they do ask for more detail, here's how I'd elaborate:

"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 is particularly elegant - it automatically subscribes when the component renders and unsubscribes when it's destroyed, preventing memory leaks without any manual cleanup."

Observable vs Promise: The Question Everyone Gets

This comparison comes up in virtually every Angular interview, so you need to know it cold. But don't just memorize a table - understand why these differences matter in practice.

Let me show you with code:

// Here's the crucial difference: Promises execute immediately
const promise = new Promise((resolve) => {
    console.log('Promise executing'); // This runs RIGHT NOW!
    resolve('done');
});
// The console.log above already fired, even though we haven't used the result
 
// Observables wait until someone subscribes
const observable = new Observable((subscriber) => {
    console.log('Observable executing'); // This 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 here's the killer feature - we can cancel it:
subscription.unsubscribe();

The practical implications of this 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.

Here's a comparison table worth memorizing, but more importantly, understand the "why" behind each row:

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

Sometimes you need to convert between them. When integrating with Promise-based libraries or when you genuinely only need a single value, here's how:

// 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'));

Hot vs Cold: The Question That Separates Juniors from Seniors

This concept trips up a lot of developers because it's rarely explained well. Let me try to make it intuitive.

Think of a cold Observable like a Netflix movie. Each viewer gets their own independent playback from the beginning. You can pause it, rewind it, and your experience is completely independent from other viewers. In RxJS terms, each subscriber triggers a fresh execution of the Observable's logic.

// This is a 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!

This has huge implications for HTTP requests. Angular's HttpClient returns cold Observables, which means each subscription triggers a new network request:

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

A hot Observable, by contrast, is like a live TV broadcast. All viewers see the same thing at the same time, regardless of when they tuned in. If you started watching late, you missed the earlier content.

// This is a 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

So how do you turn a cold Observable into a hot one when you want to share results? That's where shareReplay comes in. It's one of the most useful operators for real-world Angular development:

@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." This is perfect for data that rarely changes - the first subscriber triggers the HTTP request, and subsequent subscribers immediately receive the cached result.

Understanding Subjects: When You Need to Push Values Manually

Regular Observables are "pull-based" - they produce values when subscribed to. But sometimes you need to manually push values into a stream, like when a user clicks a button or when data arrives from a WebSocket. That's where Subjects come in.

A Subject is like a special Observable that's also an Observer - it can both produce and consume values. RxJS provides four types, each designed for different scenarios.

The Plain Subject

The basic Subject has no memory - if you emit a value before anyone subscribes, that value is lost forever.

const subject = new Subject<string>();
 
// This value is lost - no one is listening yet
subject.next('A');
 
// Now someone subscribes, but they'll never see 'A'
subject.subscribe((v) => console.log(v));
 
subject.next('B'); // Subscriber sees 'B'
subject.next('C'); // Subscriber sees 'C'

Use plain Subjects when you're broadcasting events where history doesn't matter - like button clicks or UI events.

BehaviorSubject: The One You'll Use Most

BehaviorSubject is probably the most common Subject type in Angular applications. It requires an initial value and always maintains a "current" value that new subscribers receive immediately.

// Note: we must provide an initial value
const currentUser = new BehaviorSubject<string>('guest');
 
// First subscriber immediately gets 'guest'
currentUser.subscribe((v) => console.log('Sub 1:', v)); // 'guest'
 
currentUser.next('alice');
 
// Late subscriber immediately gets 'alice' (the current value)
currentUser.subscribe((v) => console.log('Sub 2:', v)); // 'alice'
 
// You can also access the current value synchronously
console.log(currentUser.getValue()); // 'alice'

This makes BehaviorSubject perfect for state management. Here's a pattern I use constantly in Angular services:

@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);
    }
 
    // Sometimes you need synchronous access - BehaviorSubject allows this
    isLoggedIn(): boolean {
        return this.currentUserSubject.getValue() !== null;
    }
}

Notice the asObservable() call - this is a best practice that prevents other parts of your application from accidentally calling next() on your Subject. Only the AuthService should be able to change the current user.

ReplaySubject: When History Matters

ReplaySubject remembers past values and replays them to new subscribers. You specify how many values to remember.

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

This is useful for chat applications, audit logs, or any scenario where late subscribers need some context about what happened before they arrived.

AsyncSubject: The Rare One

AsyncSubject only emits its final value when it completes. It's rarely used, but occasionally useful when you only care about the end result of a long operation.

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

The Flattening Operators: switchMap, mergeMap, concatMap, exhaustMap

If there's one topic that defines whether you truly understand RxJS, it's this one. These four operators all do similar things - they transform values from an outer Observable into inner Observables - but they differ in how they handle timing and concurrency.

I'm going to explain each one with a real-world scenario, because that's how they actually make sense.

switchMap: Cancel the Previous Operation

Imagine a search autocomplete. The user types "a", and you fire an API request. Then they type "an", and you fire another request. Then "ang", another request. Then "angu", "angul", "angula", "angular".

Do you really need all seven responses? No - you only care about the result for "angular". All those intermediate requests are wasted bandwidth and potential bugs (what if "an" returns after "angular"?).

switchMap solves this by cancelling the previous inner Observable whenever a new value arrives from the outer Observable:

this.searchControl.valueChanges.pipe(
    debounceTime(300),        // Wait for typing to pause
    distinctUntilChanged(),   // Don't search if value hasn't changed
    switchMap((term) => this.searchService.search(term))
    // Each new keystroke cancels the previous HTTP request
).subscribe((results) => this.results = results);

This is also the operator you want when loading data based on route parameters. When the user navigates from /users/1 to /users/2, you want to cancel the request for user 1 and only show user 2:

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

mergeMap: Run Everything in Parallel

Now imagine you're uploading multiple files. You don't want to cancel previous uploads - you want them all to complete. And you don't necessarily need them to finish in order.

mergeMap (also known as flatMap) runs all inner Observables concurrently:

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

The second argument is optional - it limits concurrency. Without it, all files would upload simultaneously, which might overwhelm your server or the user's bandwidth.

Use mergeMap when order doesn't matter and you want maximum throughput. Parallel API calls, batch processing, fire-and-forget operations.

concatMap: One at a Time, In Order

Sometimes order does matter. Imagine a wizard-style form where you need to save step 1 before step 2, and step 2 before step 3. The server might use the result of step 1 to process step 2.

concatMap queues each inner Observable and waits for it to complete before starting the next:

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

This is also useful for database operations that must happen in sequence, or any scenario where you need guaranteed ordering.

exhaustMap: Ignore While Busy

Finally, exhaustMap is the opposite of switchMap. Instead of cancelling the previous operation when a new value arrives, it ignores new values until the current operation completes.

The classic use case is form submission. When the user clicks "Submit", you start an HTTP request. If they panic and click again, you don't want to submit twice - you want to ignore the second click.

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

How I Explain This in Interviews

Here's the one-liner I use when interviewers ask about these operators:

"I use switchMap for search and autocomplete where only the latest result matters. mergeMap for parallel operations like file uploads where I want all to complete. concatMap when order matters, like sequential form saves. And exhaustMap to prevent duplicate submissions - like stopping double-clicks on a submit button."

Error Handling That Won't Break Your App

Poor error handling is one of the easiest ways to identify inexperienced RxJS developers. If you let errors propagate unhandled, they terminate your stream - which means no more values will ever come through, even after the error condition is resolved.

Let me show you the right patterns.

The Basic Pattern

The catchError operator intercepts errors and lets you decide what to do. You have three main options:

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, I prefer Option 1 - return a fallback value that the UI can handle gracefully. Nobody likes seeing a blank screen because one API call failed.

Retry Logic for Flaky APIs

Sometimes the best response to an error is to try again. The retry operator is simple but effective:

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

For production applications, you often want exponential backoff - waiting longer between each retry to give overwhelmed servers time to recover:

this.http.get('/api/data').pipe(
    retryWhen((errors) =>
        errors.pipe(
            scan((retryCount, error) => {
                if (retryCount >= 3) throw error; // Give up after 3 retries
                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();

Keep Error Handling in Services

A pattern that's served me well: handle errors in your services, not your components. Components should receive clean data or clear error states, not raw HTTP errors:

@Injectable({ providedIn: 'root' })
export class UserService {
    getUsers(): Observable<User[]> {
        return this.http.get<User[]>('/api/users').pipe(
            retry(2),
            catchError((error) => {
                this.logService.error('Failed to fetch users', error);
                return of([]); // Return empty array instead of breaking the component
            })
        );
    }
}

The component doesn't need to know anything about HTTP errors - it just receives either users or an empty array.

The finalize Operator for Cleanup

One last pattern worth knowing: finalize 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: The Bug That Haunts Production

Memory leaks from forgotten subscriptions are one of the most common bugs in Angular applications. They're insidious because they don't cause obvious errors - your app just gradually gets slower and uses more memory until it eventually crashes.

The rule is simple: every subscription must eventually end. Either the Observable completes on its own, or you explicitly unsubscribe.

When You Need to Unsubscribe Manually

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

Infinite streams like interval() and timer() will keep ticking forever if you don't stop them. DOM events from fromEvent() will keep listening forever. Subjects you create manually will keep waiting for values. NgRx Store selects are designed to stay alive for the lifetime of the application.

When You Don't Need to Worry

Other Observables handle cleanup automatically. HttpClient requests complete after emitting one value (unless they error). The async pipe subscribes when rendering and unsubscribes when the component is destroyed - it's doing the cleanup for you. Router events are managed by Angular. Finite streams like of() and from() complete immediately.

The takeUntil Pattern

The most common cleanup pattern in Angular uses a Subject that emits when the component is destroyed:

@Component({
    selector: 'app-dashboard',
    template: `<div>{{ lastUpdate }}</div>`
})
export class DashboardComponent implements OnInit, OnDestroy {
    lastUpdate: string = '';
    private destroy$ = new Subject<void>();
 
    ngOnInit() {
        // This interval would leak without takeUntil
        interval(1000).pipe(
            takeUntil(this.destroy$)
        ).subscribe(() => {
            this.lastUpdate = new Date().toISOString();
        });
 
        // Same for DOM events
        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. Clean and automatic.

The Async Pipe: Your Best Friend

Honestly, the best way to avoid memory leaks is to use the async pipe whenever possible. It handles everything for you:

@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 subscribes when the template renders and unsubscribes when the component is destroyed. It even calls markForCheck() for you when using OnPush change detection. It's elegant and foolproof.

The Interview Question: Find the Memory Leak

This is a classic interview scenario. The interviewer shows you buggy code and asks you to find and fix 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 is straightforward:

@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 Multiple Streams

Real applications often need to coordinate multiple data sources. RxJS provides several operators for this, each with different behavior.

combineLatest: React to Any Change

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:

// Imagine a dashboard that shows user info, settings, and notifications
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, or settings change, or new notifications arrive, the dashboard updates.

forkJoin: Wait for All to Complete

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

// Load all required data before showing the page
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 }) => {
    // All three requests have completed - safe to initialize the page
    this.initializePage(users, categories, settings);
});

One 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.

merge: Interleave Multiple Sources

merge combines multiple Observables into one, emitting values from all sources as they arrive:

// Handle user interactions from any input method
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.

Practical Patterns for Real Applications

Let me share some patterns I've used repeatedly in production Angular applications.

Debounced Search with Error Handling

This is the canonical RxJS example, but here's a production-quality implementation:

@Component({
    selector: 'app-search',
    template: `
        <input [formControl]="searchControl" placeholder="Search...">
        <div *ngIf="error" class="error">{{ error }}</div>
        <div *ngFor="let result of results$ | async">{{ result.name }}</div>
    `
})
export class SearchComponent {
    searchControl = new FormControl('');
    error: string | null = null;
 
    results$ = this.searchControl.valueChanges.pipe(
        debounceTime(300),           // Don't search on every keystroke
        distinctUntilChanged(),       // Don't search if value hasn't actually changed
        filter((term) => term.length >= 2), // Require at least 2 characters
        tap(() => this.error = null), // Clear previous errors
        switchMap((term) =>
            this.searchService.search(term).pipe(
                catchError((err) => {
                    this.error = 'Search failed. Please try again.';
                    return of([]); // Return empty results on error
                })
            )
        )
    );
 
    constructor(private searchService: SearchService) {}
}

Auto-Refresh with Manual Override

A common pattern for dashboards that need to stay current:

@Component({
    selector: 'app-dashboard',
    template: `
        <button (click)="refresh$.next()">Refresh Now</button>
        <div *ngIf="data$ | async as data">
            Last updated: {{ data.timestamp }}
        </div>
    `
})
export class DashboardComponent implements OnDestroy {
    refresh$ = new Subject<void>();
    private destroy$ = new Subject<void>();
 
    data$ = merge(
        timer(0, 30000), // Auto-refresh every 30 seconds (0 means start immediately)
        this.refresh$    // Also refresh on manual click
    ).pipe(
        switchMap(() => this.dataService.getData()),
        takeUntil(this.destroy$),
        shareReplay(1)   // Cache latest value for components that subscribe later
    );
 
    constructor(private dataService: DataService) {}
 
    ngOnDestroy() {
        this.destroy$.next();
        this.destroy$.complete();
    }
}

A Proper Caching Service

When you need to cache API responses but also allow cache invalidation:

@Injectable({ providedIn: 'root' })
export class CachedDataService {
    private cache = new Map<string, Observable<any>>();
 
    getData(id: string): Observable<Data> {
        if (!this.cache.has(id)) {
            const request$ = this.http.get<Data>(`/api/data/${id}`).pipe(
                shareReplay(1),
                catchError((err) => {
                    this.cache.delete(id); // Don't cache errors
                    return throwError(() => err);
                })
            );
            this.cache.set(id, request$);
        }
        return this.cache.get(id)!;
    }
 
    invalidate(id?: string): void {
        if (id) {
            this.cache.delete(id);
        } else {
            this.cache.clear();
        }
    }
}

What Interviewers Are Actually Looking For

When I ask about RxJS in interviews, I'm not trying to trick candidates or test memorization. I'm trying to understand whether they can write maintainable, production-quality Angular code.

I want to know if you understand the difference between operators and can choose the right one for the situation. I want to know if you're aware of memory leaks and know how to prevent them. I want to see that you can handle errors gracefully instead of letting them crash your app. And I want to see that you've used these patterns in real applications, not just read about them in tutorials.

The candidates who impress me are the ones who can explain why they'd choose switchMap for a search feature, or why they prefer the async pipe over manual subscriptions. They understand the concepts deeply enough to apply them to new situations.

Quick Reference

Here's a condensed reference card to review before your interview:

ConceptKey Point
switchMapCancels previous inner Observable - use for search
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

Practice Before Your Interview

Test yourself with these questions:

1. What's the practical difference between these two implementations?

// Version A
this.searchControl.valueChanges.pipe(
    switchMap(term => this.search(term))
)
 
// Version B
this.searchControl.valueChanges.pipe(
    mergeMap(term => this.search(term))
)

2. This component has a memory leak. How would you fix it?

@Component({ template: `{{ time }}` })
export class ClockComponent implements OnInit {
    time: string = '';
 
    ngOnInit() {
        interval(1000).subscribe(() => {
            this.time = new Date().toLocaleTimeString();
        });
    }
}

3. Why might this template cause multiple HTTP requests?

// Template: {{ (users$ | async)?.length }} users, First: {{ (users$ | async)?.[0]?.name }}
users$ = this.http.get<User[]>('/api/users');

4. Which Subject type would you use for each of these scenarios?

  • Tracking the current logged-in user
  • Broadcasting button click events
  • Providing the last 5 chat messages to new subscribers

Take a minute to think through each answer before checking below.


Answers:

  1. Version A (switchMap) cancels the previous search request when the user types a new character - correct for autocomplete. Version B (mergeMap) fires all requests concurrently, so results might arrive out of order and you'd show stale data.

  2. interval(1000) never completes, so the subscription leaks when the component is destroyed. Add takeUntil(this.destroy$) in the pipe and implement ngOnDestroy to emit and complete the destroy Subject.

  3. Each async pipe creates a separate subscription, and HttpClient is cold, so two subscriptions means two HTTP requests. Fix with shareReplay(1): users$ = this.http.get<User[]>('/api/users').pipe(shareReplay(1));

  4. Current user: BehaviorSubject (you need a current value and want late subscribers to get it immediately). Button clicks: Subject (no need for history). Chat messages: ReplaySubject(5) (late subscribers should see recent history).


Dive Deeper

This is one of the most challenging topics in Angular interviews, but mastering it will set you apart. Our complete Angular interview prep guide covers 50+ questions including Components and Directives, Dependency Injection, Change Detection, Routing and Guards, Reactive Forms, Testing strategies, and Performance optimization.

Get Full Access to All Angular Questions →

Or try our free Angular preview to see more questions like this.



Related Articles

If you found this helpful, check out these related guides:

Written by the EasyInterview team, based on real interview experience from 12+ years in tech and hundreds of technical interviews conducted at companies like BNY Mellon, UBS, and leading fintech firms.

Ready to ace your interview?

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

View PDF Guides