import { Observable } from 'rxjs';
// Create an Observable
const numbers$ = new Observable<number>(subscriber => {
subscriber.next(1);
subscriber.next(2);
subscriber.next(3);
subscriber.complete();
});
// Subscribe to receive values
numbers$.subscribe({
next: value => console.log(value), // 1, 2, 3
error: err => console.error(err),
complete: () => console.log('Done')
});
Observables are lazy: no work is performed until subscribe() is called. They can emit multiple values over time, unlike Promises which resolve once. The convention is to suffix Observable variables with $.
Why it matters: Tests fundamental understanding of reactive programming and async data flow. Shows you know when and how Observables execute.
Real applications: HTTP requests, button clicks, form value changes, WebSocket messages, timers. Any async event stream in Angular uses Observables.
Common mistakes: Developers forget to subscribe thinking creating an Observable executes it. They don't unsubscribe causing memory leaks. They don't use the $ convention making code harder to read.
// Promise: eager, single value, not cancellable
const promise = fetch('/api/data')
.then(res => res.json())
.then(data => console.log(data));
// Observable: lazy, multiple values, cancellable
const obs$ = this.http.get('/api/data');
const sub = obs$.subscribe(data => console.log(data));
// Cancel the request
sub.unsubscribe();
| Feature | Promise | Observable |
|---|---|---|
| Execution | Eager | Lazy |
| Values | Single | Multiple over time |
| Cancellation | Not built-in | unsubscribe() |
| Operators | Limited (.then, .catch) | Rich (map, filter, merge...) |
Why it matters: Tests understanding of reactive vs imperative programming paradigms. Shows you know when to choose each approach.
Real applications: Use Promises for simple one-off async operations like login. Use Observables for streams: search input, real-time data, user interactions.
Common mistakes: Developers try to cancel Promises (not possible). They treat Observables like Promises expecting a single value. They don't understand laziness means Observables don't execute until subscribed.
import { Subject, BehaviorSubject, ReplaySubject, AsyncSubject } from 'rxjs';
// Subject: no initial value, late subscribers miss past emissions
const subject = new Subject<string>();
// BehaviorSubject: requires initial value, emits latest to new subscribers
const behavior = new BehaviorSubject<string>('initial');
console.log(behavior.getValue()); // 'initial'
// ReplaySubject: replays N last values to new subscribers
const replay = new ReplaySubject<string>(3); // buffer last 3
// AsyncSubject: emits only the last value, and only on complete
const async$ = new AsyncSubject<string>();
async$.next('a');
async$.next('b');
async$.complete(); // subscriber gets 'b'
BehaviorSubject is most common for state management (always has a current value). ReplaySubject is useful for caching. Use plain Subject when you only care about future emissions.
Why it matters: Tests understanding of state management in reactive applications. Shows you know when each Subject type is appropriate.
Real applications: BehaviorSubject for current user state. ReplaySubject for error recovery. AsyncSubject for buffering the final result.
Common mistakes: Developers use plain Subject for state, losing values when subscribers come late. They don't understand BehaviorSubject always emits the current value immediately. They overuse ReplaySubject buffering too much data.
Why it matters: Tests understanding of state management in reactive applications. Shows you know when each Subject type is appropriate.
Real applications: BehaviorSubject for current user state. ReplaySubject for error recovery. AsyncSubject for buffering the final result.
Common mistakes: Developers use plain Subject for state, losing values when subscribers come late. They don't understand BehaviorSubject always emits the current value immediately. They overuse ReplaySubject buffering too much data.
import { map } from 'rxjs/operators';
// Transform HTTP response
this.http.get<ApiResponse>('/api/users').pipe(
map(response => response.data),
map(users => users.filter(u => u.active)),
map(users => users.map(u => u.name))
).subscribe(names => console.log(names));
// Transform values
import { of } from 'rxjs';
of(1, 2, 3, 4, 5).pipe(
map(n => n * 10)
).subscribe(v => console.log(v));
// Output: 10, 20, 30, 40, 50
map is a transformation operator that applies a projection function to each emitted value. It does not change the number of emissions, only the values. It is one of the most frequently used RxJS operators.
Why it matters: Tests understanding of functional transformation patterns. Shows you can manipulate data through reactive pipelines.
Real applications: Extracting specific fields from API responses, formatting dates/prices, filtering arrays, combining multiple transformations.
Common mistakes: Developers perform side effects in map (use tap instead). They don't chain multiple maps for successive transformations. They forget map preserves Observable semantics (lazy, multicasted).
import { switchMap } from 'rxjs/operators';
// Search with auto-cancel of previous requests
this.searchControl.valueChanges.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(term => this.searchService.search(term))
).subscribe(results => this.results = results);
// Route param changes: auto-cancel previous HTTP call
this.route.paramMap.pipe(
switchMap(params => {
const id = Number(params.get('id'));
return this.userService.getUser(id);
})
).subscribe(user => this.user = user);
switchMap is ideal for search-as-you-type and route parameter changes because it automatically cancels the previous inner Observable when a new value arrives, preventing race conditions and stale data.
Why it matters: Tests understanding of higher-order observables and cancellation patterns. Shows you can prevent race conditions in reactive flows.
Real applications: Auto-complete searches, route parameter changes, API calls triggered by user input changes, cascading dropdown selections.
Common mistakes: Developers use mergeMap for search losing race condition protection. They don't understand switchMap cancels previous requests. They forget to add debounceTime/distinctUntilChanged for performance.
import { mergeMap } from 'rxjs/operators';
// Process all items concurrently
this.items$.pipe(
mergeMap(item => this.http.post('/api/process', item))
).subscribe(result => console.log('Processed:', result));
// With concurrency limit
this.items$.pipe(
mergeMap(item => this.http.post('/api/upload', item), 3) // max 3 concurrent
).subscribe();
// Comparison:
// switchMap: cancel previous, use for search/navigation
// mergeMap: run all concurrently, use for parallel requests
// concatMap: run one at a time in order, use for sequential operations
// exhaustMap: ignore new until current completes, use for login/submit
Use mergeMap when all inner Observables should complete independently. Use switchMap when only the latest matters. Use concatMap for sequential processing. Use exhaustMap to ignore new values while processing.
Why it matters: Tests understanding of concurrency strategies in RxJS. Shows you can choose the right operator for different scenarios.
Real applications: mergeMap for file uploads (all at once), switchMap for search (cancel old), concatMap for ordered saves, exhaustMap for form submissions (prevent double-click).
Common mistakes: Developers overuse mergeMap causing concurrent issues. They mix up when to use which operator. They don't understand the concurrency implications of each.
import { combineLatest } from 'rxjs';
// Combine multiple data sources
const user$ = this.userService.getUser(id);
const orders$ = this.orderService.getOrders(id);
const prefs$ = this.prefService.getPreferences(id);
combineLatest([user$, orders$, prefs$]).subscribe(
([user, orders, prefs]) => {
this.user = user;
this.orders = orders;
this.preferences = prefs;
}
);
// Filter + sort combined
combineLatest([this.items$, this.filter$, this.sort$]).pipe(
map(([items, filter, sort]) => {
let result = items.filter(i => i.category === filter);
return result.sort((a, b) => a[sort] - b[sort]);
})
).subscribe(filtered => this.filtered = filtered);
combineLatest waits until all source Observables have emitted at least once, then emits on every subsequent change. It is ideal for combining multiple reactive data sources.import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
export class UserListComponent implements OnInit, OnDestroy {
private destroy$ = new Subject<void>();
ngOnInit() {
this.userService.getUsers().pipe(
takeUntil(this.destroy$)
).subscribe(users => this.users = users);
this.route.paramMap.pipe(
takeUntil(this.destroy$)
).subscribe(params => this.loadUser(params.get('id')!));
interval(5000).pipe(
takeUntil(this.destroy$)
).subscribe(() => this.refresh());
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}
Create a Subject, pipe takeUntil(destroy$) on every subscription, and call next() and complete() in ngOnDestroy. This cleanly unsubscribes from all Observables at once.
Why it matters: Tests understanding of memory leak prevention and subscription management. Shows you can build long-lived components safely.
Real applications: Components that load user data, respond to route changes, listen to WebSocket events. Any component with multiple subscriptions.
Common mistakes: Developers forget to unsubscribe causing memory leaks. They create destroy$ but don't pipe it on every subscription. They manually track multiple subscriptions instead of using one subject.
import { debounceTime, distinctUntilChanged } from 'rxjs/operators';
// Search input with debounce
@Component({
template: '<input (input)="onSearch($event)" />'
})
export class SearchComponent implements OnInit {
private searchSubject = new Subject<string>();
ngOnInit() {
this.searchSubject.pipe(
debounceTime(300), // wait 300ms after last keystroke
distinctUntilChanged(), // skip if same value as previous
switchMap(term => this.searchService.search(term))
).subscribe(results => this.results = results);
}
onSearch(event: Event) {
const term = (event.target as HTMLInputElement).value;
this.searchSubject.next(term);
}
}
debounceTime is essential for user input scenarios. The 300ms delay reduces API calls to only trigger after the user stops typing. Combine with distinctUntilChanged to avoid redundant calls.
Why it matters: Tests understanding of performance optimization for user input. Shows you can prevent unnecessary API calls.
Real applications: Search boxes that query on every keystroke would be slow. autocomplete fields, live filtering with 300ms debounce reduces server load.
Common mistakes: Developers don't debounce rapid user input causing excessive API calls. They use throttle when they should use debounce. They debounce without distinctUntilChanged still making redundant calls.
import { catchError, of, EMPTY } from 'rxjs';
// Return a default value on error
this.http.get<User[]>('/api/users').pipe(
catchError(error => {
console.error('Failed to load users:', error);
return of([]); // return empty array as fallback
})
).subscribe(users => this.users = users);
// Rethrow with custom error
this.http.get('/api/data').pipe(
catchError(error => {
if (error.status === 401) {
this.router.navigate(['/login']);
return EMPTY; // complete without emitting
}
return throwError(() => new Error('Server error'));
})
).subscribe();
// Retry then catch
this.http.get('/api/data').pipe(
retry(2),
catchError(err => of({ error: true, message: err.message }))
).subscribe();
catchError must return an Observable. Use of() for default values, EMPTY to complete silently, or throwError() to propagate a transformed error. Place catchError after retry to catch only the final failure.
Why it matters: Tests understanding of error handling in reactive chains. Shows you can build resilient async operations.
Real applications: HTTP errors return cached data or empty array. 401 Unauthorized redirects to login. Network retries fail then show offline message.
Common mistakes: Developers don't catch errors letting observables fail silently. They forget catchError must return an Observable causing type errors. They place catchError before retry, catching each retry instead of final failure.
// switchMap: cancel previous (search)
search$.pipe(switchMap(term => api.search(term)));
// mergeMap: run all concurrently (parallel uploads)
files$.pipe(mergeMap(file => api.upload(file)));
// concatMap: run one after another (ordered saves)
saves$.pipe(concatMap(data => api.save(data)));
// exhaustMap: ignore new until done (login button)
click$.pipe(exhaustMap(() => api.login(credentials)));
Use switchMap for search/navigation, mergeMap for parallel tasks, concatMap for ordered sequential operations, and exhaustMap to prevent duplicate submissions.
Why it matters: Tests understanding of higher-order Observable operators and concurrency control. Shows you know when to use each flattening strategy.
Real applications: switchMap for search (cancel old), mergeMap for file uploads (all at once), concatMap for sequential saves, exhaustMap for login (prevent double-click).
Common mistakes: Developers mix up which operator to use causing wrong behavior. They use mergeMap everywhere creating race conditions. They don't understand the concurrency models.
// Manual subscribe: must handle cleanup yourself
export class UserComponent implements OnDestroy {
private sub!: Subscription;
users: User[] = [];
ngOnInit() {
this.sub = this.userService.getUsers()
.subscribe(users => this.users = users);
}
ngOnDestroy() { this.sub.unsubscribe(); }
}
// Async pipe: automatic subscribe and cleanup
// Template: <li *ngFor="let user of users$ | async">{{ user.name }}</li>
users$ = this.userService.getUsers();
Prefer the async pipe when possible for cleaner code and automatic cleanup. Use subscribe when you need to perform side effects or complex logic with the data.
Why it matters: Tests understanding of Angular template integration and memory leak prevention. Shows you can write clean, maintainable templates.
Real applications: Displaying user lists, showing loading states, rendering data from multiple sources with | async in templates.
Common mistakes: Developers subscribe in component class when async pipe would work. They forget to unsubscribe when using subscribe manually. They use multiple async pipes on same observable causing multiple subscriptions.
import { forkJoin } from 'rxjs';
// Wait for all requests to complete
forkJoin({
user: this.http.get<User>('/api/user/1'),
orders: this.http.get<Order[]>('/api/orders'),
prefs: this.http.get<Prefs>('/api/preferences')
}).subscribe(({ user, orders, prefs }) => {
this.user = user;
this.orders = orders;
this.preferences = prefs;
});
forkJoin only works with Observables that complete, not long-lived streams. Add catchError on individual requests if you want partial results when one fails.
Why it matters: Tests understanding of combining asynchronous operations. Shows you can manage multiple concurrent requests efficiently.
Real applications: Loading user data, orders, and preferences simultaneously. Page initialization that needs multiple independent API calls.
Common mistakes: Developers use forkJoin with never-completing observables. They don't handle one failure meaning everything fails. They use combineLatest when they meant forkJoin.
import { of, from, interval } from 'rxjs';
// of: emit values and complete
of(1, 2, 3).subscribe(v => console.log(v)); // 1, 2, 3, complete
// from: convert array or promise
from([10, 20, 30]).subscribe(v => console.log(v)); // 10, 20, 30
from(fetch('/api/data')).subscribe(resp => console.log(resp));
// interval: emit number every N ms
interval(1000).pipe(take(5)).subscribe(v => console.log(v));
// 0, 1, 2, 3, 4 (one per second)
of() is for static values, from() is for converting existing data structures, and interval() is for time-based emissions. Use take() with interval to limit emissions.
Why it matters: Tests understanding of Observable creation patterns. Shows you know different ways to generate observables from data sources.
Real applications: of() for hardcoded option lists, from() for array conversions, interval() for polling or heartbeat timers.
Common mistakes: Developers use of() with arrays and don't understand it spreads arguments. They forget to take() from interval causing infinite emissions. They use from() for single values when of() is simpler.
// takeUntil pattern
private destroy$ = new Subject<void>();
ngOnInit() {
this.data$.pipe(takeUntil(this.destroy$)).subscribe();
this.events$.pipe(takeUntil(this.destroy$)).subscribe();
}
ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); }
// Angular 16+: takeUntilDestroyed
constructor() {
this.data$.pipe(takeUntilDestroyed()).subscribe();
}
Always unsubscribe from long-lived Observables like interval, WebSocket connections, and store selectors. HTTP requests complete automatically but should still be cleaned up if the component may be destroyed before the response arrives.
Why it matters: Tests understanding of memory management in long-lived applications. Shows you can prevent performance degradation over time.
Real applications: Apps with hundreds of components created/destroyed over user session. Memory leaks compound causing slowdowns and browser crashes.
Common mistakes: Developers don't track subscriptions causing memory leaks. They create multiple destroy$ subjects instead of one per component. They don't test for leaks with Chrome DevTools heap snapshots.