Lesson 19 of 30
A Promise resolves once. An Observable is a stream you can cancel mid-flight — which is exactly what a search box hammering an API on every keystroke needs.
18 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
You already know Promises: you kick off async work, you get back one value (or one error), once. RxJS Observables model something slightly different — a stream of values over time, which might emit zero times, once, or repeatedly, and which you can cancel while it's in flight.
Angular leans on RxJS in a few specific, load-bearing places: HttpClient methods return Observables (not Promises), and the async pipe in templates knows how to subscribe to an Observable and unsubscribe automatically when the component is destroyed. That's the whole reason RxJS shows up in nearly every real Angular codebase — it's not bolted on, it's what the HTTP layer speaks natively.
The core moves: you subscribe() to start listening (nothing happens until you do — Observables are lazy), and you compose transformations with .pipe() and operators like map, filter, and switchMap.
A naive implementation fires an HTTP request on every keystroke, and a slow request for "cat" can resolve after a faster request for "cats" — showing stale results. This is exactly the shape RxJS was built for:
import { Component, inject } from '@angular/core';
import { AsyncPipe } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import { Subject } from 'rxjs';
import { debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
interface Result { id: string; title: string; }
@Component({
selector: 'app-search-box',
standalone: true,
imports: [AsyncPipe], // @for/@if are compiler-native blocks, no import needed
template: `
<input (input)="onInput($any($event.target).value)" />
@for (r of results$ | async; track r.id) {
<div>{{ r.title }}</div>
}
`,
})
export class SearchBoxComponent {
private http = inject(HttpClient);
private query$ = new Subject<string>();
results$ = this.query$.pipe(
debounceTime(300), // wait for a pause in typing
distinctUntilChanged(), // skip re-firing on an unchanged value
switchMap((term) =>
this.http.get<Result[]>(`/api/search?q=${term}`),
),
);
onInput(term: string) {
this.query$.next(term);
}
}
The operator doing the real work is switchMap: every time a new search term comes through, it cancels whatever previous HTTP request was still in flight and switches to a new one. A Promise-based fetch can't do that on its own — there's no built-in "cancel the last one" — you'd need to hand-roll it with an AbortController per request. switchMap gives you that cancellation as a one-line consequence of composing operators.
The async pipe in the template subscribes to results$ when the component initializes and unsubscribes when it's destroyed — you never call .subscribe() yourself, and there's no manual cleanup to forget.
.pipe() chains, it doesn't mutate. Each operator inside .pipe() returns a new Observable wrapping the previous one — query$.pipe(debounceTime(300)) doesn't change query$ itself, so you can safely derive multiple different streams from the same source.Subject is both an Observable and something you can push into. query$.next(term) is what makes it different from a plain Observable — you're both the source pushing values and the thing components subscribe to..subscribe() directly in a component instead of using the async pipe, you're responsible for unsubscribing in ngOnDestroy — skip that and you've got a listener outliving the component, quietly leaking.If closures are "remembers where it came from," an Observable is "a Promise that can happen more than once and can be told to stop." Everything from that first lesson about a function holding a live reference still applies — switchMap's callback closes over nothing special, but the Subject itself is exactly the same kind of mutable, closed-over reference that made the debounce example work, just wrapped in RxJS's vocabulary instead of a raw setTimeout.