- Creation Operators
- Transformation Operators
- Filtering Operators
- Combination Operators
- Error Handling Operators
- Utility Operators
- Multicasting Operators
- Common Angular Patterns
- Operator Comparison Guide
- Interview Tips & Tricks
Create new Observables from various sources
What it does: Creates an Observable that emits the arguments you provide, then completes.
Signature: of(...values)
When to use:
- Return mock/static data
- Testing services
- Default/fallback values
Example:
of(1, 2, 3).subscribe(val => console.log(val));
// Output: 1, 2, 3, then completes
// Angular service example
getUserMock(): Observable<User> {
return of({ id: 1, name: 'John' });
}Interview Note: of() emits synchronously and completes immediately.
What it does: Converts arrays, promises, iterables, or observables into an Observable.
Signature: from(input)
When to use:
- Convert arrays to streams
- Convert Promises to Observables
- Iterate over any iterable
Example:
// From array
from([10, 20, 30]).subscribe(val => console.log(val));
// From Promise
from(fetch('/api/data')).subscribe(response => console.log(response));
// From string (iterable)
from('hello').subscribe(char => console.log(char)); // h, e, l, l, oInterview Note: Unlike of(), from([1,2,3]) emits each item separately, while of([1,2,3]) emits the entire array as one value.
What it does: Emits sequential numbers at specified time intervals (in milliseconds).
Signature: interval(period)
When to use:
- Polling APIs
- Auto-refresh functionality
- Timers and countdowns
Example:
interval(1000).subscribe(val => console.log(val));
// Output: 0 (after 1s), 1 (after 2s), 2 (after 3s)...
// Angular: Poll API every 5 seconds
interval(5000).pipe(
switchMap(() => this.http.get('/api/status'))
).subscribe(status => this.status = status);Interview Note: Starts counting from 0. Never completes unless you use operators like take().
What it does: Emits a value after a delay, optionally repeating at intervals.
Signature: timer(initialDelay, period?)
When to use:
- Delayed operations
- Scheduled tasks
- One-time delays
Example:
// Emit once after 3 seconds
timer(3000).subscribe(() => console.log('3 seconds passed'));
// Wait 2s, then emit every 1s
timer(2000, 1000).subscribe(val => console.log(val));
// Output: 0 (after 2s), 1 (after 3s), 2 (after 4s)...
// Angular: Delayed notification
timer(5000).subscribe(() => this.showNotification());Interview Note: timer(0) emits immediately (async). timer(3000) is like setTimeout, timer(0, 1000) is like setInterval.
What it does: Creates Observable lazily - only when subscribed to.
Signature: defer(observableFactory)
When to use:
- Lazy evaluation
- Create fresh Observable per subscription
- Decision logic at subscription time
Example:
const random$ = defer(() => of(Math.random()));
random$.subscribe(val => console.log(val)); // 0.234
random$.subscribe(val => console.log(val)); // 0.789 (different value!)
// Angular: Conditional observable creation
defer(() => {
if (this.isLoggedIn) {
return this.http.get('/api/user-data');
} else {
return of(null);
}
});Transform values emitted by Observables
What it does: Transforms each emitted value using a projection function.
Signature: map(project: (value, index) => any)
When to use:
- Transform/modify data
- Extract properties
- Convert data types
Example:
from([1, 2, 3]).pipe(
map(x => x * 10)
).subscribe(val => console.log(val)); // 10, 20, 30
// Angular: Extract data from HTTP response
this.http.get<ApiResponse>('/api/users').pipe(
map(response => response.data.users)
).subscribe(users => this.users = users);
// With index
from(['a', 'b', 'c']).pipe(
map((letter, index) => `${index}: ${letter}`)
).subscribe(console.log); // 0: a, 1: b, 2: cInterview Note: Synchronous transformation. Returns non-Observable values. For Observable results, use switchMap, mergeMap, etc.
What it does: Maps to an inner Observable, cancels previous inner Observable when new value arrives.
Signature: switchMap(project: (value) => Observable)
When to use:
- HTTP requests that should cancel previous (search, autocomplete)
- Navigation scenarios
- Latest value matters, not all values
Example:
// Search with autocomplete
searchInput$.pipe(
debounceTime(300),
switchMap(term => this.http.get(`/api/search?q=${term}`))
).subscribe(results => this.results = results);
// Angular routing
this.route.params.pipe(
switchMap(params => this.http.get(`/api/user/${params.id}`))
).subscribe(user => this.user = user);Visual:
Source: --1--2--3--|
switchMap(x => interval(1000))
Inner 1: --0--1--2--X (cancelled)
Inner 2: ----0--1--X (cancelled)
Inner 3: ------0--1--2--3--|
Result: ------0--1--2--3--|
Interview Note: CANCELS previous. Use when only latest result matters. Most common operator in Angular!
What it does: Maps to inner Observable, maintains all subscriptions concurrently.
Signature: mergeMap(project: (value) => Observable, concurrent?: number)
When to use:
- Multiple concurrent HTTP requests
- File uploads (process all simultaneously)
- Order doesn't matter, want all results
Example:
// Fetch multiple users concurrently
from([1, 2, 3]).pipe(
mergeMap(id => this.http.get(`/api/user/${id}`))
).subscribe(user => console.log(user));
// All 3 HTTP requests happen at the same time
// Limit concurrency
from(fileArray).pipe(
mergeMap(file => this.uploadFile(file), 2) // Max 2 uploads at once
).subscribe(result => console.log(result));Visual:
Source: --1--2--3--|
mergeMap(x => interval(1000))
Inner 1: --0--1--2--3--4--|
Inner 2: ----0--1--2--3--4--|
Inner 3: ------0--1--2--3--4--|
Result: --0-01-012-123-234-34-4--|
Interview Note: Does NOT cancel. All inner Observables run simultaneously. Can cause memory issues if uncontrolled.
What it does: Maps to inner Observable, waits for each to complete before starting next.
Signature: concatMap(project: (value) => Observable)
When to use:
- Sequential operations (order critical)
- Queue processing
- One-at-a-time HTTP requests
Example:
// Process items sequentially
from([1, 2, 3]).pipe(
concatMap(id => this.http.post(`/api/process/${id}`, data))
).subscribe(result => console.log(result));
// Request 2 starts only after Request 1 completes
// Angular: Sequential animations
from(['step1', 'step2', 'step3']).pipe(
concatMap(step => this.animateStep(step))
).subscribe();Visual:
Source: --1--2--3--|
concatMap(x => timer(1000))
Inner 1: --0|
Inner 2: --0|
Inner 3: --0|
Result: --0--0--0|
Interview Note: Maintains ORDER. Slower than mergeMap but guarantees sequence. Good for dependent operations.
What it does: Maps to inner Observable, IGNORES new values while inner is active.
Signature: exhaustMap(project: (value) => Observable)
When to use:
- Prevent duplicate form submissions
- Login buttons (ignore rapid clicks)
- Prevent spam clicking
Example:
// Login button - ignore clicks while logging in
loginButton$.pipe(
exhaustMap(() => this.authService.login(credentials))
).subscribe(result => this.handleLoginResult(result));
// Form submission
this.form.submit$.pipe(
exhaustMap(() => this.http.post('/api/save', this.form.value))
).subscribe(response => this.showSuccess());Visual:
Source: --1--2--3--4--5--|
exhaustMap(x => timer(2000))
Inner 1: --0|
Inner 3: --0|
Inner 5: --0|
Result: --0------0------0|
(2 and 4 are ignored)
Interview Note: Opposite of switchMap. Keeps current, ignores new. Perfect for preventing duplicate actions.
What it does: Flattens higher-order Observable (Observable of Observables).
When to use:
- After
map()returns Observables - Flattening nested streams
Example:
// Instead of:
source$.pipe(
map(x => this.http.get(`/api/${x}`)),
mergeAll()
)
// You typically use:
source$.pipe(
mergeMap(x => this.http.get(`/api/${x}`))
)Interview Note: mergeMap = map + mergeAll, switchMap = map + switchAll, concatMap = map + concatAll
What it does: Accumulator function (like reduce() but emits intermediate values).
Signature: scan(accumulator, seed?)
When to use:
- Running totals
- Maintaining state over time
- Building up values
Example:
from([1, 2, 3, 4]).pipe(
scan((acc, val) => acc + val, 0)
).subscribe(console.log);
// Output: 1, 3, 6, 10
// Angular: Cart total
this.cartItems$.pipe(
scan((total, item) => total + item.price, 0)
).subscribe(total => this.cartTotal = total);
// State management
actions$.pipe(
scan((state, action) => this.reducer(state, action), initialState)
).subscribe(state => this.state = state);Interview Note: Emits on each value. Use reduce() if you only want the final result.
What it does: Extracts nested property from each emitted object.
Signature: pluck(...properties: string[])
When to use:
- Extract specific properties
- Simplify nested access
Example:
from([
{ name: 'John', age: 30 },
{ name: 'Jane', age: 25 }
]).pipe(
pluck('name')
).subscribe(console.log); // 'John', 'Jane'
// Nested properties
from([
{ user: { profile: { name: 'John' } } }
]).pipe(
pluck('user', 'profile', 'name')
).subscribe(console.log); // 'John'
// Angular: Extract from response
this.http.get('/api/data').pipe(
pluck('data', 'users')
).subscribe(users => this.users = users);Interview Note: Deprecated in RxJS 8, use map(x => x.property) instead. Still appears in legacy code.
What it does: Reduces values to a single value, emits only when source completes.
Signature: reduce(accumulator, seed?)
When to use:
- Final aggregated result
- Similar to Array.reduce()
Example:
from([1, 2, 3, 4]).pipe(
reduce((acc, val) => acc + val, 0)
).subscribe(console.log); // Only emits: 10
// Angular: Calculate total
this.orders$.pipe(
reduce((total, order) => total + order.amount, 0)
).subscribe(total => this.grandTotal = total);Interview Note: Only emits ONCE when source completes. Use scan() for intermediate values.
Control which values pass through the stream
What it does: Emits only values that pass the predicate function.
Signature: filter(predicate: (value, index) => boolean)
When to use:
- Conditional logic
- Remove unwanted values
- Data filtering
Example:
from([1, 2, 3, 4, 5, 6]).pipe(
filter(x => x % 2 === 0)
).subscribe(console.log); // 2, 4, 6
// Angular: Filter form changes
this.form.valueChanges.pipe(
filter(value => value.email.includes('@')),
filter(value => value.password.length >= 8)
).subscribe(validValue => this.enableSubmit());
// With index
from(['a', 'b', 'c']).pipe(
filter((val, index) => index > 0)
).subscribe(console.log); // 'b', 'c'Interview Note: Synchronous check. Returns boolean. Can filter by index too.
What it does: Emits value only after specified time has passed without another emission.
Signature: debounceTime(dueTime: number)
When to use:
- Search input (wait for user to stop typing)
- Window resize events
- Auto-save after pause
Example:
searchInput$.pipe(
debounceTime(300)
).subscribe(value => this.search(value));
// Angular: Search autocomplete
this.searchControl.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(term => this.http.get(`/api/search?q=${term}`))
).subscribe(results => this.results = results);Visual:
Source: --1--2--3--------4--5----|
debounceTime(20ms)
Result: --------3------------5---|
Interview Note: Waits for "quiet period". Delays by specified time. Classic use: search inputs.
What it does: Emits first value, then ignores values for duration.
Signature: throttleTime(duration: number, config?)
When to use:
- Rate limiting (scroll, mouse move)
- Button click prevention
- Performance optimization for high-frequency events
Example:
fromEvent(window, 'scroll').pipe(
throttleTime(1000)
).subscribe(event => this.handleScroll(event));
// Handles scroll at most once per second
// Button clicks
saveButton$.pipe(
throttleTime(2000)
).subscribe(() => this.save());
// Allows one save per 2 secondsVisual:
Source: --1--2--3--4--5--6--7--|
throttleTime(20ms)
Result: --1--------4--------7--|
Interview Note: Emits FIRST value in window. debounceTime emits LAST. Use throttleTime for events that should execute immediately but not too often.
What it does: Only emits when current value differs from previous value.
Signature: distinctUntilChanged(compare?)
When to use:
- Prevent duplicate emissions
- Optimize change detection
- Avoid redundant API calls
Example:
from([1, 1, 2, 2, 3, 1]).pipe(
distinctUntilChanged()
).subscribe(console.log); // 1, 2, 3, 1
// Angular: Form control
this.formControl.valueChanges.pipe(
distinctUntilChanged()
).subscribe(value => console.log('Actually changed:', value));
// Custom comparison
this.users$.pipe(
distinctUntilChanged((prev, curr) => prev.id === curr.id)
).subscribe(user => this.currentUser = user);Interview Note: Only compares with PREVIOUS value (not all values). For comparing with all, use distinct().
What it does: Emits values that haven't been emitted before.
Signature: distinct(keySelector?, flushes?)
When to use:
- Remove all duplicates from stream
- Unique values only
Example:
from([1, 2, 1, 3, 2, 4]).pipe(
distinct()
).subscribe(console.log); // 1, 2, 3, 4
// With key selector
from([
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' },
{ id: 1, name: 'John Doe' }
]).pipe(
distinct(user => user.id)
).subscribe(console.log); // Only first occurrence of each idInterview Note: Keeps track of ALL previous values (memory consideration). Use distinctUntilChanged() for consecutive duplicates only.
What it does: Emits only the first N values, then completes.
Signature: take(count: number)
When to use:
- Limit emissions
- Take first N items
- One-time operations
Example:
interval(1000).pipe(
take(5)
).subscribe(console.log); // 0, 1, 2, 3, 4, then completes
// Angular: Get first value
this.route.params.pipe(
take(1)
).subscribe(params => console.log('Initial params:', params));Interview Note: Automatically completes after N emissions. Great for limiting infinite streams.
What it does: Emits until another Observable (notifier) emits.
Signature: takeUntil(notifier: Observable)
When to use:
- Unsubscribe pattern in Angular
- Stop streams based on events
- Component lifecycle management
Example:
// Angular: Standard unsubscribe pattern
private destroy$ = new Subject<void>();
ngOnInit() {
interval(1000).pipe(
takeUntil(this.destroy$)
).subscribe(val => console.log(val));
this.dataService.getData().pipe(
takeUntil(this.destroy$)
).subscribe(data => this.data = data);
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
// Stop on button click
source$.pipe(
takeUntil(stopButton$)
).subscribe(val => console.log(val));Interview Note: THE standard unsubscribe pattern in Angular. When notifier emits ANY value, source completes.
What it does: Emits while predicate returns true, completes when false.
Signature: takeWhile(predicate: (value) => boolean, inclusive?)
When to use:
- Conditional completion
- Take until condition fails
Example:
from([1, 2, 3, 4, 5]).pipe(
takeWhile(x => x < 4)
).subscribe(console.log); // 1, 2, 3
// With inclusive flag
from([1, 2, 3, 4, 5]).pipe(
takeWhile(x => x < 4, true)
).subscribe(console.log); // 1, 2, 3, 4
// Angular: Load until quota reached
this.loadItems().pipe(
scan((acc, item) => acc + item.size, 0),
takeWhile(totalSize => totalSize < maxQuota)
).subscribe(size => this.currentSize = size);What it does: Skips the first N values from source.
Signature: skip(count: number)
When to use:
- Ignore initial emissions
- Skip first N values
Example:
from([1, 2, 3, 4, 5]).pipe(
skip(2)
).subscribe(console.log); // 3, 4, 5
// Angular: Skip initial form value
this.form.valueChanges.pipe(
skip(1) // Skip initial value
).subscribe(changes => this.handleUserChange(changes));What it does: Skips emissions until notifier Observable emits.
Signature: skipUntil(notifier: Observable)
When to use:
- Wait for initialization
- Start processing after event
Example:
source$.pipe(
skipUntil(initComplete$)
).subscribe(val => console.log(val));
// Angular: Wait for auth
this.dataStream$.pipe(
skipUntil(this.authService.authenticated$)
).subscribe(data => this.processData(data));What it does: Emits only the first value (or first matching predicate), then completes.
Signature: first(predicate?, defaultValue?)
When to use:
- Get first emission
- Find first matching value
- One-time snapshot
Example:
from([1, 2, 3, 4, 5]).pipe(
first()
).subscribe(console.log); // 1
// With predicate
from([1, 2, 3, 4, 5]).pipe(
first(x => x > 3)
).subscribe(console.log); // 4
// With default (if no match)
from([1, 2, 3]).pipe(
first(x => x > 10, 999)
).subscribe(console.log); // 999
// Angular: Get initial route param
this.route.params.pipe(
first()
).subscribe(params => this.initialId = params.id);Interview Note: Completes after first emission. Throws error if source completes without emitting (unless default provided).
What it does: Emits only the last value before source completes.
Signature: last(predicate?, defaultValue?)
When to use:
- Get final value
- Find last matching value
Example:
from([1, 2, 3, 4, 5]).pipe(
last()
).subscribe(console.log); // 5
from([1, 2, 3, 4, 5]).pipe(
last(x => x < 4)
).subscribe(console.log); // 3Interview Note: Only emits when source COMPLETES. Won't work with never-ending streams.
What it does: Emits most recent value when notifier emits.
Signature: sample(notifier: Observable)
When to use:
- Snapshot values at intervals
- Periodic sampling
Example:
source$.pipe(
sample(interval(1000))
).subscribe(val => console.log(val));
// Emits latest source value every second
// Angular: Sample user input on save
this.formChanges$.pipe(
sample(this.saveButton$)
).subscribe(formValue => this.save(formValue));What it does: Similar to throttle, but emits LAST value instead of first.
Signature: auditTime(duration: number)
When to use:
- Rate limiting with latest value
- Like throttle but want most recent
Example:
clicks$.pipe(
auditTime(1000)
).subscribe(click => this.handleClick(click));Visual:
Source: --1--2--3--4--5--6--7--|
auditTime(20ms)
Result: -----2--------5--------7|
Interview Note: throttleTime emits first in window, auditTime emits last in window.
Combine multiple Observables
What it does: Combines multiple Observables, emits when ANY emits (after all have emitted at least once).
Signature: combineLatest([obs1, obs2, ...]) or combineLatest({key1: obs1, key2: obs2})
When to use:
- Form validation (multiple fields)
- Combine dependent streams
- Dashboard with multiple data sources
- Real-time calculations
Example:
// Array syntax
combineLatest([
this.firstName$,
this.lastName$
]).subscribe(([first, last]) => {
this.fullName = `${first} ${last}`;
});
// Object syntax (cleaner)
combineLatest({
user: this.userService.getUser(),
settings: this.settingsService.getSettings(),
permissions: this.permService.getPermissions()
}).subscribe(({user, settings, permissions}) => {
this.initializeApp(user, settings, permissions);
});
// Angular: Complex form validation
combineLatest([
this.emailControl.valueChanges,
this.passwordControl.valueChanges,
this.termsControl.valueChanges
]).pipe(
map(([email, password, terms]) =>
this.isValidEmail(email) &&
password.length >= 8 &&
terms === true
)
).subscribe(isValid => this.submitButton.disabled = !isValid);Visual:
A: --1-----2-----3--|
B: ----a-----b-----c|
combineLatest([A, B])
Result: ----[1,a]-[2,a]-[2,b]-[3,b]-[3,c]|
Interview Note:
- Waits for ALL sources to emit at least once
- Then emits on EVERY emission from any source
- Always has latest value from each source
- Good for reactive forms
What it does: Waits for ALL Observables to COMPLETE, then emits last value from each as array/object.
Signature: forkJoin([obs1, obs2, ...]) or forkJoin({key1: obs1, key2: obs2})
When to use:
- Parallel HTTP requests (wait for all)
- Load multiple resources before proceeding
- Initialize app with multiple API calls
- Like Promise.all()
Example:
// Array syntax
forkJoin([
this.http.get('/api/users'),
this.http.get('/api/posts'),
this.http.get('/api/comments')
]).subscribe(([users, posts, comments]) => {
console.log('All loaded', users, posts, comments);
});
// Object syntax (recommended)
forkJoin({
users: this.http.get('/api/users'),
roles: this.http.get('/api/roles'),
permissions: this.http.get('/api/permissions')
}).pipe(
finalize(() => this.loading = false)
).subscribe(({users, roles, permissions}) => {
this.users = users;
this.roles = roles;
this.permissions = permissions;
});
// Angular: Component initialization
ngOnInit() {
this.loading = true;
forkJoin({
profile: this.userService.getProfile(),
notifications: this.notificationService.getAll(),
config: this.configService.load()
}).subscribe({
next: (data) => this.initializeComponent(data),
error: (err) => this.handleError(err),
complete: () => this.loading = false
});
}Visual:
A: --1--2--3--|
B: ----a--b--c--|
C: ------x--y--z--|
forkJoin([A, B, C])
Result: -----------[3, c, z]|
Interview Note:
- Like Promise.all() - parallel execution
- Only emits ONCE when ALL complete
- If any Observable errors, entire forkJoin errors
- If any Observable never completes, forkJoin never emits
- Perfect for loading multiple independent resources
What it does: Merges multiple Observables into one, emitting all values as they occur.
Signature: merge(...observables, concurrent?)
When to use:
- Combine multiple event sources
- Handle multiple user actions
- Aggregate similar streams
Example:
merge(
this.saveButton$,
this.keyboardShortcut$,
this.autoSave$
).subscribe(() => this.save());
// Angular: Multiple action triggers
merge(
this.refreshButton$.pipe(map(() => 'button')),
this.interval$.pipe(map(() => 'interval')),
this.websocket$.pipe(map(() => 'websocket'))
).pipe(
switchMap(source => {
console.log('Refresh triggered by:', source);
return this.http.get('/api/data');
})
).subscribe(data => this.data = data);
// Merge error handlers
merge(
this.userErrors$,
this.networkErrors$,
this.validationErrors$
).subscribe(error => this.showError(error));Visual:
A: --1----2----3--|
B: ----a----b----c|
merge(A, B)
Result: --1-a--2-b--3-c|
Interview Note:
- All subscriptions happen immediately
- Flattens multiple sources into one
- First-come, first-served emission
- Completes when ALL sources complete
What it does: Subscribes to Observables sequentially (one after previous completes).
Signature: concat(...observables)
When to use:
- Sequential operations
- Animation sequences
- Ordered API calls
Example:
concat(
this.http.post('/api/create', data),
this.http.put('/api/update', moreData),
this.http.get('/api/verify')
).subscribe(result => console.log(result));
// Angular: Sequential animations
concat(
this.fadeOut(),
this.slideIn(),
this.highlight()
).subscribe();Visual:
A: --1--2--|
B: ----a--b--|
C: ------x--y--|
concat(A, B, C)
Result: --1--2-----a--b------x--y--|
Interview Note:
- Maintains strict ORDER
- Waits for each to complete before starting next
- Like Promise chaining with .then()
- One subscription at a time
What it does: Subscribes to all Observables, emits from the first one that emits, unsubscribes from others.
Signature: race(...observables)
When to use:
- Fallback mechanisms
- Fastest response wins
- Timeout patterns
Example:
// First API to respond wins
race(
this.http.get('/api/server1/data'),
this.http.get('/api/server2/data'),
this.http.get('/api/server3/data')
).subscribe(data => console.log('Fastest response:', data));
// Timeout pattern
race(
this.http.get('/api/data'),
timer(5000).pipe(
switchMap(() => throwError(() => new Error('Timeout')))
)
).subscribe({
next: data => this.data = data,
error: err => this.handleTimeout(err)
});Interview Note: "Winner takes all" - first emission determines the winner.
What it does: Combines Observables by pairing values by index (1st with 1st, 2nd with 2nd, etc.).
Signature: zip(...observables)
When to use:
- Pair related data
- Synchronized streams
- Combine by position
Example:
zip(
from([1, 2, 3]),
from(['a', 'b', 'c']),
from([10, 20, 30])
).subscribe(([num, letter, value]) => {
console.log(num, letter, value);
});
// Output: [1, 'a', 10], [2, 'b', 20], [3, 'c', 30]
// Angular: Combine parallel streams
zip(
interval(100),
from(['Loading', 'Processing', 'Complete'])
).subscribe(([count, message]) => {
this.status = `${message} (${count})`;
});Visual:
A: --1----2----3----|
B: ------a----b----c|
zip(A, B)
Result: ------[1,a]-[2,b]-[3,c]|
Interview Note:
- Waits for corresponding index from each source
- Completes when ANY source completes
- Creates "tuples" of paired values
What it does: Combines source with latest values from other Observables when source emits.
Signature: source.pipe(withLatestFrom(...observables))
When to use:
- Add context to events
- Include current state with actions
- Primary/secondary relationship
Example:
// Add context to clicks
buttonClick$.pipe(
withLatestFrom(currentUser$, appSettings$)
).subscribe(([click, user, settings]) => {
console.log('Click with context:', { click, user, settings });
});
// Angular: Form submission with current state
this.submitButton$.pipe(
withLatestFrom(
this.formValue$,
this.isValid$,
this.permissions$
)
).subscribe(([_, formData, isValid, permissions]) => {
if (isValid && permissions.canSubmit) {
this.save(formData);
}
});Visual:
A (source): --1----2----3----|
B: ------a--------b-|
withLatestFrom(B)
Result: ------[2,a]-[3,b]|
Interview Note:
- Only emits when SOURCE emits
- Uses latest value from others (doesn't wait)
- Others must have emitted at least once
- Different from combineLatest (which emits on any change)
What it does: Emits specified value(s) before source begins emitting.
Signature: startWith(...values)
When to use:
- Provide initial value
- Default state
- Loading states
Example:
this.http.get('/api/data').pipe(
startWith([]) // Empty array while loading
).subscribe(data => this.data = data);
// Angular: Search with initial state
this.searchResults$ = this.searchTerm$.pipe(
debounceTime(300),
switchMap(term => this.search(term)),
startWith([]) // Show empty initially
);
// Loading indicator
this.loading$ = this.dataStream$.pipe(
map(() => false),
startWith(true)
);Interview Note: Emits synchronously before subscription. Great for initial values in templates.
What it does: Emits specified value(s) after source completes.
Signature: endWith(...values)
When to use:
- Append final values
- Cleanup indicators
Example:
from([1, 2, 3]).pipe(
endWith(999)
).subscribe(console.log); // 1, 2, 3, 999What it does: Groups consecutive emissions as pairs [previous, current].
Signature: pairwise()
When to use:
- Compare consecutive values
- Track changes
- Detect trends
Example:
from([1, 2, 3, 4, 5]).pipe(
pairwise()
).subscribe(console.log);
// [1,2], [2,3], [3,4], [4,5]
// Angular: Detect scroll direction
fromEvent(window, 'scroll').pipe(
map(() => window.scrollY),
pairwise(),
map(([prev, curr]) => curr > prev ? 'down' : 'up')
).subscribe(direction => this.scrollDirection = direction);What it does: Catches errors and returns a new Observable or rethrows.
Signature: catchError((error, caught) => Observable)
When to use:
- HTTP error handling
- Fallback values
- Error recovery
- Prevent stream termination
Example:
// Return fallback value
this.http.get('/api/data').pipe(
catchError(error => {
console.error('Error occurred:', error);
return of([]); // Return empty array
})
).subscribe(data => this.data = data);
// Return different Observable
this.http.get('/api/primary').pipe(
catchError(() => this.http.get('/api/backup'))
).subscribe(data => this.data = data);
// Re-throw with modification
this.http.get('/api/data').pipe(
catchError(error => {
console.error('API Error:', error);
return throwError(() => new Error('Custom error message'));
})
).subscribe({
next: data => this.data = data,
error: err => this.showError(err)
});
// Angular: Graceful degradation
this.userService.getUser(id).pipe(
catchError(error => {
if (error.status === 404) {
return of(this.getDefaultUser());
}
return throwError(() => error);
})
).subscribe(user => this.user = user);Interview Note:
- Must return an Observable
- Stream continues with returned Observable
- Place it where you want to handle errors
- Can be used multiple times in chain
What it does: Resubscribes to source Observable N times on error.
Signature: retry(count?: number) or retry({count, delay})
When to use:
- Network failures
- Transient errors
- Resilience patterns
Example:
// Retry 3 times
this.http.get('/api/data').pipe(
retry(3),
catchError(error => of([]))
).subscribe(data => this.data = data);
// Retry with delay (RxJS 7+)
this.http.get('/api/data').pipe(
retry({
count: 3,
delay: 1000 // Wait 1s between retries
}),
catchError(error => of([]))
).subscribe(data => this.data = data);
// Angular: Robust API call
this.apiService.fetchData().pipe(
retry({ count: 3, delay: 2000 }),
catchError(error => {
this.showError('Failed after 3 retries');
return of(null);
})
).subscribe(data => this.handleData(data));Interview Note:
- Retries immediately by default
- Use with catchError to provide fallback
- Total attempts = 1 + retry count
What it does: Resubscribes based on custom logic (with delays, conditions).
Signature: retryWhen(notifier: (errors: Observable) => Observable)
When to use:
- Exponential backoff
- Conditional retry
- Custom retry strategies
Example:
// Exponential backoff
this.http.get('/api/data').pipe(
retryWhen(errors => errors.pipe(
mergeMap((error, index) => {
const retryAttempt = index + 1;
if (retryAttempt > 3) {
return throwError(() => error);
}
console.log(`Retry attempt ${retryAttempt}, waiting ${retryAttempt * 1000}ms`);
return timer(retryAttempt * 1000);
})
))
).subscribe(data => this.data = data);
// Conditional retry
this.http.get('/api/data').pipe(
retryWhen(errors => errors.pipe(
mergeMap(error => {
if (error.status === 503) { // Service unavailable
return timer(5000); // Retry after 5s
}
return throwError(() => error); // Don't retry other errors
}),
take(5)
))
).subscribe(data => this.data = data);Interview Note:
- More control than retry()
- Return Observable to retry
- Return error to give up
- Complex but powerful
What it does: Continues to next Observable even if previous errors.
Signature: onErrorResumeNext(...observables)
When to use:
- Best-effort operations
- Continue despite failures
Example:
onErrorResumeNext(
this.http.get('/api/data1'),
this.http.get('/api/data2'),
this.http.get('/api/data3')
).subscribe(data => console.log(data));
// Even if data1 fails, continues to data2, then data3What it does: Performs side effects without modifying the stream.
Signature: tap(observer or next, error?, complete?)
When to use:
- Logging/debugging
- Analytics tracking
- Side effects
- Debugging streams
Example:
// Simple logging
this.http.get('/api/users').pipe(
tap(data => console.log('Received:', data)),
map(data => data.users),
tap(users => console.log('Mapped to:', users))
).subscribe(users => this.users = users);
// Full observer
source$.pipe(
tap({
next: val => console.log('Next:', val),
error: err => console.error('Error:', err),
complete: () => console.log('Complete')
})
).subscribe();
// Angular: Loading states and analytics
this.userService.loadUser(id).pipe(
tap(() => this.loading = true),
tap(user => this.analytics.track('user_loaded', user.id)),
finalize(() => this.loading = false)
).subscribe(user => this.user = user);
// Debug complex chain
this.form.valueChanges.pipe(
tap(val => console.log('1. Form value:', val)),
debounceTime(300),
tap(val => console.log('2. After debounce:', val)),
distinctUntilChanged(),
tap(val => console.log('3. After distinct:', val)),
switchMap(val => this.validate(val))
).subscribe(result => console.log('4. Final result:', result));Interview Note:
- Does NOT modify stream values
- Perfect for debugging without breaking chain
- Use for side effects that don't affect data flow
- Original name was
do()(deprecated)
What it does: Delays emissions by specified time.
Signature: delay(due: number | Date)
When to use:
- Simulate network delays
- Timed operations
- Animation sequencing
Example:
// Delay by milliseconds
of('Hello').pipe(
delay(2000)
).subscribe(msg => console.log(msg)); // After 2 seconds
// Delay until date
of('Message').pipe(
delay(new Date('2024-12-31'))
).subscribe();
// Angular: Delayed notification
this.saveSuccess$.pipe(
delay(500),
tap(() => this.showNotification('Saved!'))
).subscribe();
// Stagger animations
from([1, 2, 3]).pipe(
concatMap(num => of(num).pipe(
delay(num * 500),
tap(n => this.animateItem(n))
))
).subscribe();What it does: Delays each emission based on another Observable.
Signature: delayWhen(delayDurationSelector)
When to use:
- Dynamic delays
- Wait for events
- Conditional timing
Example:
source$.pipe(
delayWhen(val => timer(val * 1000))
).subscribe();
// Angular: Wait for initialization
this.userActions$.pipe(
delayWhen(() => this.appInitialized$)
).subscribe(action => this.handleAction(action));What it does: Errors if Observable doesn't emit within specified time.
Signature: timeout(due: number | Date)
When to use:
- Slow API detection
- SLA enforcement
- Prevent hanging
Example:
this.http.get('/api/data').pipe(
timeout(5000),
catchError(error => {
if (error.name === 'TimeoutError') {
console.error('Request timed out');
return of([]);
}
return throwError(() => error);
})
).subscribe(data => this.data = data);
// Angular: User action timeout
this.waitingForInput$.pipe(
timeout(30000),
catchError(() => {
this.showMessage('Are you still there?');
return of(null);
})
).subscribe();What it does: Executes callback when Observable completes or errors (cleanup).
Signature: finalize(callback: () => void)
When to use:
- Cleanup operations
- Hide loading spinners
- Release resources
- Finally block equivalent
Example:
// Always hide loader
this.http.get('/api/data').pipe(
tap(() => this.loading = true),
finalize(() => this.loading = false)
).subscribe({
next: data => this.data = data,
error: err => this.showError(err)
});
// Angular: Comprehensive cleanup
this.processData().pipe(
tap(() => {
this.loading = true;
this.error = null;
}),
finalize(() => {
this.loading = false;
this.saveState();
console.log('Operation completed or failed');
})
).subscribe({
next: result => this.result = result,
error: err => this.error = err
});Interview Note:
- Runs on BOTH complete and error
- Like finally in try/catch
- Perfect for cleanup
- Runs AFTER unsubscribe
What it does: Resubscribes to source after it completes.
Signature: repeat(count?: number)
When to use:
- Polling that resets
- Retry successful operations
Example:
// Repeat 3 times
of('Hello').pipe(
repeat(3)
).subscribe(console.log); // Hello, Hello, Hello
// Infinite repeat
interval(1000).pipe(
take(3),
repeat() // Repeats indefinitely
).subscribe(console.log); // 0,1,2,0,1,2,0,1,2...What it does: Wraps each value with timestamp.
Signature: timestamp(scheduler?)
When to use:
- Track emission times
- Performance monitoring
Example:
from([1, 2, 3]).pipe(
timestamp()
).subscribe(console.log);
// {value: 1, timestamp: 1639234567890}
// {value: 2, timestamp: 1639234567891}What it does: Emits time elapsed between emissions.
Signature: timeInterval(scheduler?)
When to use:
- Measure time between events
- Performance tracking
Example:
interval(1000).pipe(
take(3),
timeInterval()
).subscribe(console.log);
// {value: 0, interval: 1000}
// {value: 1, interval: 1000}What it does: Shares a single subscription among multiple subscribers (multicast).
Signature: share()
When to use:
- Avoid duplicate HTTP requests
- Share expensive operations
- Convert cold to hot Observable
Example:
// Without share - 2 HTTP requests
const data$ = this.http.get('/api/data');
data$.subscribe(d => console.log('Sub 1:', d));
data$.subscribe(d => console.log('Sub 2:', d));
// Makes 2 separate HTTP calls
// With share - 1 HTTP request
const shared$ = this.http.get('/api/data').pipe(share());
shared$.subscribe(d => console.log('Sub 1:', d));
shared$.subscribe(d => console.log('Sub 2:', d));
// Makes only 1 HTTP call, shares result
// Angular: Shared data service
@Injectable()
export class DataService {
private data$ = this.http.get('/api/data').pipe(
share()
);
getData() {
return this.data$; // Multiple components share one request
}
}Interview Note:
- Creates one subscription, multiple observers
- Resets when all subscribers unsubscribe
- New subscriber after completion triggers new subscription
What it does: Shares subscription AND replays N last values to new subscribers.
Signature: shareReplay(bufferSize, windowTime?, scheduler?)
When to use:
- Cache HTTP responses
- Share initialization data
- Late subscribers need past values
Example:
// Cache user data
private user$ = this.http.get<User>('/api/user').pipe(
shareReplay(1) // Cache last value
);
getUser() {
return this.user$; // All subscribers get cached value
}
// Angular: Application config
@Injectable({ providedIn: 'root' })
export class ConfigService {
private config$ = this.http.get('/api/config').pipe(
shareReplay({
bufferSize: 1,
refCount: true // Auto-cleanup when no subscribers
})
);
getConfig() {
return this.config$;
}
}
// Multiple components
// Component 1
this.configService.getConfig().subscribe(config => {...});
// Component 2 (subscribes later)
this.configService.getConfig().subscribe(config => {...});
// Both get same cached result, only 1 HTTP call madeInterview Note:
- Replays buffer to late subscribers
- Doesn't reset like share()
- Use bufferSize: 1 for caching
- refCount: true for memory cleanup
- Most common caching pattern in Angular
What it does: Converts cold Observable to hot, uses Subject internally.
Signature: publish() then connect()
When to use:
- Manual control over subscription timing
- Advanced multicasting scenarios
Example:
const source$ = interval(1000).pipe(
take(3),
publish()
);
source$.subscribe(val => console.log('Sub 1:', val));
source$.subscribe(val => console.log('Sub 2:', val));
// Start emitting (only when ready)
const subscription = source$.connect();
// Later: subscription.unsubscribe();Interview Note: Rarely used in modern Angular. Use share() or shareReplay() instead.
The Classic Autocomplete Pattern
@Component({
selector: 'app-search',
template: `
<input [formControl]="searchControl" placeholder="Search...">
<div *ngFor="let result of results$ | async">{{result}}</div>
`
})
export class SearchComponent implements OnInit {
searchControl = new FormControl('');
results$: Observable<any[]>;
ngOnInit() {
this.results$ = this.searchControl.valueChanges.pipe(
debounceTime(300), // Wait 300ms after typing stops
distinctUntilChanged(), // Only if value changed
filter(term => term.length >= 2), // Min 2 characters
switchMap(term => // Cancel previous, start new search
this.searchService.search(term).pipe(
catchError(() => of([])) // Handle errors gracefully
)
)
);
}
}Why this pattern:
debounceTime: Prevents API spam while user typesdistinctUntilChanged: Skips duplicate searchesfilter: Avoids searching empty/short stringsswitchMap: Cancels outdated searchescatchError: Graceful error handling
The Memory Leak Prevention Pattern
@Component({...})
export class MyComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
ngOnInit() {
// All subscriptions use takeUntil
this.dataService.getData().pipe(
takeUntil(this.destroy$)
).subscribe(data => this.data = data);
this.userService.getUser().pipe(
takeUntil(this.destroy$)
).subscribe(user => this.user = user);
interval(1000).pipe(
takeUntil(this.destroy$)
).subscribe(tick => this.tick = tick);
}
ngOnDestroy() {
this.destroy$.next(); // Trigger completion
this.destroy$.complete(); // Clean up Subject
}
}Why this pattern:
- Single destroy$ for all subscriptions
- Automatic unsubscribe on component destroy
- Prevents memory leaks
- Clean and maintainable
Alternative using Async Pipe (better):
@Component({
template: `<div>{{data$ | async}}</div>`
})
export class MyComponent {
// No manual subscription needed!
data$ = this.dataService.getData();
}The Parallel Data Loading Pattern
@Component({...})
export class DashboardComponent implements OnInit {
loading = false;
error: string | null = null;
ngOnInit() {
this.loadData();
}
loadData() {
this.loading = true;
this.error = null;
forkJoin({
users: this.userService.getUsers(),
posts: this.postService.getPosts(),
comments: this.commentService.getComments(),
stats: this.analyticsService.getStats()
}).pipe(
finalize(() => this.loading = false)
).subscribe({
next: ({users, posts, comments, stats}) => {
this.users = users;
this.posts = posts;
this.comments = comments;
this.stats = stats;
this.initializeDashboard();
},
error: (error) => {
this.error = 'Failed to load dashboard data';
console.error(error);
}
});
}
}Why this pattern:
- Loads all resources in parallel
- Single loading state
- All-or-nothing approach
- finalize() handles both success/error
The Multi-Field Validation Pattern
@Component({...})
export class RegistrationComponent implements OnInit {
emailControl = new FormControl('');
passwordControl = new FormControl('');
confirmControl = new FormControl('');
termsControl = new FormControl(false);
isValid$: Observable<boolean>;
validationErrors$: Observable<string[]>;
ngOnInit() {
this.isValid$ = combineLatest([
this.emailControl.valueChanges.pipe(startWith('')),
this.passwordControl.valueChanges.pipe(startWith('')),
this.confirmControl.valueChanges.pipe(startWith('')),
this.termsControl.valueChanges.pipe(startWith(false))
]).pipe(
map(([email, password, confirm, terms]) => {
return this.isValidEmail(email) &&
password.length >= 8 &&
password === confirm &&
terms === true;
})
);
this.validationErrors$ = combineLatest([
this.emailControl.valueChanges,
this.passwordControl.valueChanges,
this.confirmControl.valueChanges
]).pipe(
map(([email, password, confirm]) => {
const errors: string[] = [];
if (!this.isValidEmail(email)) {
errors.push('Invalid email');
}
if (password.length < 8) {
errors.push('Password too short');
}
if (password !== confirm) {
errors.push('Passwords do not match');
}
return errors;
})
);
}
isValidEmail(email: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
}The User-Friendly Update Pattern
@Component({...})
export class TodoComponent {
todos$ = this.todoService.getTodos();
toggleTodo(todo: Todo) {
// Optimistic update
const updatedTodo = { ...todo, completed: !todo.completed };
this.updateTodoLocally(updatedTodo);
this.todoService.update(updatedTodo).pipe(
catchError(error => {
// Rollback on error
this.updateTodoLocally(todo);
this.showError('Update failed');
return throwError(() => error);
})
).subscribe();
}
updateTodoLocally(todo: Todo) {
// Update local state immediately
this.todos$ = this.todos$.pipe(
map(todos => todos.map(t =>
t.id === todo.id ? todo : t
))
);
}
}The Controlled Polling Pattern
@Component({...})
export class StatusComponent implements OnInit {
private polling$ = new Subject<boolean>();
status$: Observable<Status>;
ngOnInit() {
this.status$ = this.polling$.pipe(
switchMap(shouldPoll =>
shouldPoll
? interval(5000).pipe(
startWith(0),
switchMap(() => this.statusService.getStatus())
)
: EMPTY
)
);
}
startPolling() {
this.polling$.next(true);
}
stopPolling() {
this.polling$.next(false);
}
}The Resilient API Call Pattern
@Injectable()
export class ApiService {
getData() {
return this.http.get('/api/data').pipe(
retryWhen(errors => errors.pipe(
mergeMap((error, index) => {
const retryAttempt = index + 1;
// Give up after 5 attempts
if (retryAttempt > 5) {
return throwError(() => error);
}
// Don't retry on 4xx errors (client errors)
if (error.status >= 400 && error.status < 500) {
return throwError(() => error);
}
// Exponential backoff: 1s, 2s, 4s, 8s, 16s
const delayMs = Math.pow(2, retryAttempt) * 1000;
console.log(`Retry attempt ${retryAttempt}, waiting ${delayMs}ms`);
return timer(delayMs);
})
)),
catchError(error => {
console.error('All retry attempts failed', error);
return of(null);
})
);
}
}The Smart Search Pattern
@Component({...})
export class TypeAheadComponent {
searchControl = new FormControl('');
suggestions$: Observable<string[]>;
ngOnInit() {
this.suggestions$ = this.searchControl.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged(),
filter(term => term.length >= 3), // Minimum 3 characters
switchMap(term =>
this.searchService.getSuggestions(term).pipe(
catchError(() => of([])),
startWith([]) // Clear previous results immediately
)
)
);
}
}| Operator | Behavior | Use When | Example |
|---|---|---|---|
| map | Transform values synchronously | Simple transformation, no Observables | Transform API response |
| switchMap | Cancel previous, use latest | Latest result matters, cancellable | Search, navigation |
| mergeMap | Run all concurrently | All results matter, order doesn't | Batch file uploads |
| concatMap | Run sequentially | Order matters, queue processing | Sequential API calls |
| exhaustMap | Ignore new while busy | Prevent duplicate actions | Form submit, login |
// Example: Processing IDs [1, 2, 3]
// switchMap - Latest wins, cancels previous
from([1, 2, 3]).pipe(
switchMap(id => this.http.get(`/api/${id}`))
)
// If 3 arrives while 1 is loading, 1 and 2 are cancelled
// Result: Only gets data for ID 3
// mergeMap - All run concurrently
from([1, 2, 3]).pipe(
mergeMap(id => this.http.get(`/api/${id}`))
)
// All 3 requests run simultaneously
// Result: Gets all 3, order may vary
// concatMap - Sequential, waits for each
from([1, 2, 3]).pipe(
concatMap(id => this.http.get(`/api/${id}`))
)
// Request 2 waits for 1, request 3 waits for 2
// Result: Gets all 3, in order 1, 2, 3
// exhaustMap - Ignores new while busy
from([1, 2, 3]).pipe(
exhaustMap(id => this.http.get(`/api/${id}`))
)
// If 2 and 3 arrive while 1 is loading, they're ignored
// Result: Only gets data for ID 1// User typing: a-b-c-d-e (50ms apart)
// debounceTime(200) - Wait for pause
// Emits: 'e' (after 200ms of silence)
// Use: Search input
// throttleTime(200) - Emit first, block for duration
// Emits: 'a', then after 200ms can emit again
// Use: Scroll events, rate limiting
// auditTime(200) - Emit last after duration
// Emits: Last value in each 200ms window
// Use: Similar to throttle but want latest value
fromEvent(input, 'keyup').pipe(
// Pick one based on need:
debounceTime(300) // ✓ Search: wait for pause
// throttleTime(300) // ✓ Scroll: rate limit
// auditTime(300) // ✓ Latest value periodically
)const a$ = interval(100).pipe(take(3)); // 0, 1, 2
const b$ = interval(150).pipe(take(3)); // 0, 1, 2
// combineLatest - Emits when ANY emits (after all emit once)
combineLatest([a$, b$])
// Emits: [0,0], [1,0], [1,1], [2,1], [2,2]
// Use: Form validation, reactive calculations
// forkJoin - Emits ONCE when ALL complete
forkJoin([a$, b$])
// Emits: [2, 2] (only at the end)
// Use: Parallel HTTP requests, wait for all
// withLatestFrom - Emits when source emits
a$.pipe(withLatestFrom(b$))
// Emits: [0,0], [1,0], [2,1]
// Only emits when a$ emits, uses latest b$
// Use: Add context to primary streamconst a$ = of(1, 2, 3);
const b$ = of('a', 'b', 'c');
// merge - Interleave all emissions
merge(a$, b$)
// Output: 1, 2, 3, 'a', 'b', 'c' (order may vary)
// concat - Sequential (one after another)
concat(a$, b$)
// Output: 1, 2, 3, 'a', 'b', 'c' (guaranteed order)
// zip - Pair by index
zip(a$, b$)
// Output: [1,'a'], [2,'b'], [3,'c']// share - No replay, resets when all unsubscribe
const shared$ = this.http.get('/api').pipe(share());
sub1 = shared$.subscribe(); // Makes HTTP call
sub2 = shared$.subscribe(); // Shares same call
sub1.unsubscribe();
sub2.unsubscribe();
sub3 = shared$.subscribe(); // Makes NEW HTTP call
// shareReplay - Replays to late subscribers, never resets
const cached$ = this.http.get('/api').pipe(
shareReplay(1)
);
sub1 = cached$.subscribe(); // Makes HTTP call
sub2 = cached$.subscribe(); // Gets cached value
sub1.unsubscribe();
sub2.unsubscribe();
sub3 = cached$.subscribe(); // Still gets cached value (no new call)// take(n) - First n emissions
source$.pipe(take(3)) // Takes first 3
// first() - First emission only (can filter)
source$.pipe(first()) // Takes very first
source$.pipe(first(x => x > 5)) // First that matches
// takeUntil(notifier) - Until notifier emits
source$.pipe(
takeUntil(destroy$)
) // Unsubscribe patternQ1: What's the difference between hot and cold Observables?
Answer:
-
Cold: Creates new execution for each subscriber (unicast)
- Examples: HTTP calls, Promises converted to Observables
- Each subscriber triggers the producer independently
const cold$ = this.http.get('/api/data'); cold$.subscribe(); // Makes HTTP call cold$.subscribe(); // Makes ANOTHER HTTP call
-
Hot: Shares execution among subscribers (multicast)
- Examples: DOM events, Subjects, shared Observables
- Producer exists independently of subscribers
const hot$ = fromEvent(button, 'click'); hot$.subscribe(); // Listens to existing clicks hot$.subscribe(); // Also listens to same clicks
Convert cold to hot:
const hot$ = cold$.pipe(share());Q2: Explain the difference between Subject, BehaviorSubject, and ReplaySubject
Answer:
| Type | Description | Initial Value | Replay |
|---|---|---|---|
| Subject | Basic Subject, no initial value, no replay | No | No |
| BehaviorSubject | Requires initial value, replays latest | Yes | 1 |
| ReplaySubject | No initial value, replays N values | No | N |
| AsyncSubject | Only emits last value on complete | No | 1 (on complete) |
// Subject - No replay
const subject = new Subject<number>();
subject.next(1);
subject.subscribe(val => console.log(val)); // Nothing
subject.next(2); // Logs: 2
// BehaviorSubject - Replays latest (requires initial)
const behavior = new BehaviorSubject<number>(0);
behavior.next(1);
behavior.subscribe(val => console.log(val)); // Logs: 1
behavior.next(2); // Logs: 2
// ReplaySubject - Replays last N values
const replay = new ReplaySubject<number>(2);
replay.next(1);
replay.next(2);
replay.next(3);
replay.subscribe(val => console.log(val)); // Logs: 2, 3Angular Use Cases:
- Subject: Event bus, notifications
- BehaviorSubject: Current state (logged in user, current page)
- ReplaySubject: Caching, late subscribers need history
Q3: How do you prevent memory leaks in Angular components?
Answer:
Method 1: takeUntil (Recommended)
private destroy$ = new Subject<void>();
ngOnInit() {
this.service.data$.pipe(
takeUntil(this.destroy$)
).subscribe(data => this.data = data);
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}Method 2: Async Pipe (Best)
// Template
data$ = this.service.getData();
// In template: {{ data$ | async }}
// No manual unsubscribe needed!Method 3: Manual Subscription Storage
private subscriptions = new Subscription();
ngOnInit() {
this.subscriptions.add(
this.service.data$.subscribe(...)
);
}
ngOnDestroy() {
this.subscriptions.unsubscribe();
}Q4: When would you use switchMap vs mergeMap?
Answer:
Use switchMap when:
- Latest result matters, cancel previous
- Search/autocomplete
- Navigation (route changes)
- User typing
// Search - cancel old searches
searchTerm$.pipe(
switchMap(term => this.search(term))
)Use mergeMap when:
- All results matter
- Concurrent operations
- Order doesn't matter
// Upload multiple files
from(files).pipe(
mergeMap(file => this.upload(file))
)Use concatMap when:
- Order matters
- Sequential processing
// Process queue in order
from(tasks).pipe(
concatMap(task => this.process(task))
)Use exhaustMap when:
- Ignore new while busy
- Prevent duplicates
// Login button - ignore spam clicks
loginButton$.pipe(
exhaustMap(() => this.login())
)Q5: How do you handle errors in Observable chains?
Answer:
Local error handling (within pipe):
this.http.get('/api/data').pipe(
catchError(error => {
console.error(error);
return of([]); // Fallback value
})
).subscribe(data => this.data = data);Global error handling (at subscribe):
this.http.get('/api/data').subscribe({
next: data => this.data = data,
error: error => this.handleError(error),
complete: () => console.log('Done')
});Retry with backoff:
this.http.get('/api/data').pipe(
retry(3),
catchError(error => {
this.showError('Failed after 3 retries');
return of([]);
})
).subscribe(data => this.data = data);Q6: What's the purpose of shareReplay and when would you use it?
Answer:
Purpose: Cache HTTP responses and share among subscribers.
// Without shareReplay - Multiple HTTP calls
private getUserData() {
return this.http.get<User>('/api/user');
}
ngOnInit() {
this.getUserData().subscribe(user => this.user = user);
this.getUserData().subscribe(user => this.userName = user.name);
// Makes 2 HTTP requests!
}
// With shareReplay - Single HTTP call
private user$ = this.http.get<User>('/api/user').pipe(
shareReplay(1)
);
ngOnInit() {
this.user$.subscribe(user => this.user = user);
this.user$.subscribe(user => this.userName = user.name);
// Makes only 1 HTTP request, shares result
}Use Cases:
- Cache user profile
- Cache application config
- Share expensive computations
- Reference data that doesn't change often
- *Use trackBy in ngFor with Observables
// Template
<div *ngFor="let item of items$ | async; trackBy: trackById">
// Component
trackById(index: number, item: any) {
return item.id;
}- Unsubscribe from finite vs infinite streams
// HTTP calls complete automatically - no unsubscribe needed
this.http.get('/api/data').subscribe(...);
// Infinite streams MUST be unsubscribed
interval(1000).pipe(
takeUntil(this.destroy$)
).subscribe(...);- Avoid nested subscriptions (subscribe inside subscribe)
// ❌ Bad - Nested subscriptions
this.service1.getData().subscribe(data1 => {
this.service2.getData(data1).subscribe(data2 => {
// Nested hell
});
});
// ✅ Good - Use switchMap
this.service1.getData().pipe(
switchMap(data1 => this.service2.getData(data1))
).subscribe(data2 => {
// Clean and flat
});- Use shareReplay for expensive operations
// ❌ Bad - Recalculates for each subscriber
getExpensiveData() {
return this.http.get('/api/data').pipe(
map(data => this.expensiveTransform(data))
);
}
// ✅ Good - Calculate once, share result
private expensiveData$ = this.http.get('/api/data').pipe(
map(data => this.expensiveTransform(data)),
shareReplay(1)
);
getExpensiveData() {
return this.expensiveData$;
}1. Not unsubscribing from infinite streams
// ❌ Memory leak
ngOnInit() {
interval(1000).subscribe(...);
}
// ✅ Proper cleanup
ngOnInit() {
interval(1000).pipe(
takeUntil(this.destroy$)
).subscribe(...);
}2. Using nested subscriptions instead of operators
// ❌ Callback hell
this.getUser().subscribe(user => {
this.getPosts(user.id).subscribe(posts => {
this.getComments(posts[0].id).subscribe(comments => {
// Deeply nested
});
});
});
// ✅ Flat and readable
this.getUser().pipe(
switchMap(user => this.getPosts(user.id)),
switchMap(posts => this.getComments(posts[0].id))
).subscribe(comments => {
// Clean!
});3. Not handling errors in chains
// ❌ Error breaks entire stream
source$.pipe(
switchMap(val => this.http.get(`/api/${val}`))
).subscribe(...);
// ✅ Handle errors gracefully
source$.pipe(
switchMap(val => this.http.get(`/api/${val}`).pipe(
catchError(() => of(null))
))
).subscribe(...);4. Subscribing multiple times to cold Observables
// ❌ Makes 3 HTTP calls
const data$ = this.http.get('/api/data');
data$.subscribe(d => this.data1 = d);
data$.subscribe(d => this.data2 = d);
data$.subscribe(d => this.data3 = d);
// ✅ Make 1 HTTP call
const data$ = this.http.get('/api/data').pipe(share());
data$.subscribe(d => this.data1 = d);
data$.subscribe(d => this.data2 = d);
data$.subscribe(d => this.data3 = d);5. Using Subject instead of BehaviorSubject for state
// ❌ New subscribers miss current state
private userSubject = new Subject<User>();
user$ = this.userSubject.asObservable();
// ✅ New subscribers get current state
private userSubject = new BehaviorSubject<User>(null);
user$ = this.userSubject.asObservable();Creating Observables
of(1, 2, 3) // Static values
from([1, 2, 3]) // From array/promise
interval(1000) // Every 1 second
timer(3000) // After 3 seconds
fromEvent(el, 'click') // DOM eventsTransforming
map(x => x * 2) // Transform value
switchMap(x => obs$) // Cancel previous
mergeMap(x => obs$) // All concurrent
concatMap(x => obs$) // Sequential
exhaustMap(x => obs$) // Ignore while busy
scan((acc, x) => acc + x) // AccumulateFiltering
filter(x => x > 5) // Conditional
debounceTime(300) // Wait for pause
throttleTime(1000) // Rate limit
distinctUntilChanged() // Skip duplicates
take(5) // First 5
takeUntil(notifier$) // Until signal
first() // First value onlyCombining
combineLatest([a$, b$]) // Latest from all
forkJoin([a$, b$]) // Wait for all complete
merge(a$, b$) // Interleave
concat(a$, b$) // Sequential
zip(a$, b$) // Pair by index
withLatestFrom(b$) // Context to primaryError Handling
catchError(() => of([])) // Fallback
retry(3) // Retry count
retryWhen(errors$ => ...) // Custom retryUtility
tap(val => console.log(val)) // Side effects
delay(1000) // Delay emissions
finalize(() => cleanup()) // Cleanup
timeout(5000) // TimeoutMulticasting
share() // Share subscription
shareReplay(1) // Cache and shareTop 10 Must-Know Operators:
- map - Transform data
- switchMap - Cancel previous (search, navigation)
- mergeMap - Concurrent operations
- combineLatest - Reactive forms, multiple sources
- forkJoin - Parallel HTTP requests
- debounceTime - Search input
- takeUntil - Unsubscribe pattern
- catchError - Error handling
- tap - Debugging, side effects
- shareReplay - Caching
Key Concepts to Master:
- Hot vs Cold Observables
- Subject types and when to use each
- Memory leak prevention (takeUntil, async pipe)
- Error handling strategies
- When to use which "map" operator
- Difference between combineLatest and forkJoin
Practice Scenarios:
- Build a search with autocomplete
- Implement form validation
- Create a data service with caching
- Handle file uploads with progress
- Implement retry logic with backoff
- Build a polling mechanism
Ready for deep dive questions! 🚀