In RxJS, why does exhaustMap suit a login button and a slow polling loop, and what exactly happens to the values it ignores?
answer
- busy means deaf
- nothing is buffered
- no overlapping requests
- timer ticks skipped while waiting
basics
~20 sexhaustMap starts an inner observable only when none is active and discards source values that arrive meanwhile, without buffering them. A login button then sends one request however often it is clicked, and a poll never overlaps a slow previous request.
solid answer
~40 s`exhaustMap(project)` holds at most one inner subscription. When a source value arrives and no inner is active, it projects the value and subscribes; while that inner is running, further source values are **dropped** — not queued, not replayed later. For a login button, `clicks$.pipe(exhaustMap(() => login(credentials)))` sends one request per attempt, however many times an impatient user clicks. For polling, `timer(0, 5000).pipe(exhaustMap(() => fetchStatus()))` never lets requests pile up: if a response takes eight seconds, the ticks during that time are skipped and the next poll starts at the following tick. Its output completes when the source has completed and the active inner has finished.
code
ts · 14 linesimport { Observable, Subject, exhaustMap, timer } from 'rxjs';
declare function login(user: string, password: string): Observable<{ token: string }>;
declare function fetchStatus(): Observable<string>;
const submit$ = new Subject<{ user: string; password: string }>();
const session$ = submit$.pipe(
exhaustMap(({ user, password }) => login(user, password))
);
const status$ = timer(0, 5000).pipe(
exhaustMap(() => fetchStatus())
);go deeper
Recall that exhaustMap ignores new values while its current inner observable is running, which prevents double submissions.
Explain that ignored values are discarded, not buffered, and compare the effect with concatMap and switchMap on a submit button.
Apply it to polling against a slow server and explain why the alternatives either pile up requests or never complete one.
Decide where duplicate-submission protection belongs across UI, client stream and server idempotency, and how those layers back each other up.
## The rule: busy means deaf `exhaustMap` is the flattening operator that **ignores**. In RxJS 7.8: 1. A source value arrives while **no inner** is active: `project(value, index)` is called and the result is subscribed. 2. A source value arrives while an inner **is** active: the value is not projected, not stored, not counted — it is gone. 3. When the inner completes, the operator is ready again; the **next** source value starts a new inner. 4. When the source completes, the output completes as soon as no inner is active. That differs from the other three strategies: | operator | value arriving while busy | |---|---| | `exhaustMap` | discarded | | `concatMap` | buffered, run later in order | | `mergeMap` | run immediately in parallel | | `switchMap` | run immediately; the busy inner is cancelled | ## Login and other submit buttons A login click must produce **one** request per attempt: - `mergeMap` sends a request per click, possibly logging in twice or tripping rate limits; - `concatMap` queues the extra clicks and sends them after the first finishes; - `switchMap` cancels the first request and sends the second — the server may process both; - `exhaustMap` sends the first and ignores clicks until it completes. If the login fails, the inner completes (after handling its error), the operator becomes idle, and the next click is a genuine retry. Disabling the button in the template helps the user, but the operator is the guarantee: a keyboard submit or a second trigger path goes through the same stream. ## Polling without overlap Polling with a fixed timer and a request per tick is a classic place for requests to pile up when the server slows down: ```ts timer(0, 5000).pipe(exhaustMap(() => fetchStatus())) ``` | tick | request state | effect | |---|---|---| | 0 s | idle | request 1 starts | | 5 s | request 1 still running (takes 8 s) | tick ignored | | 8 s | request 1 completes | idle | | 10 s | idle | request 2 starts | With `mergeMap`, a slow server would receive ever more concurrent polls; with `switchMap`, each tick would cancel a request that might never get the time to finish, so a server slower than the interval would never produce a result. `exhaustMap` degrades gracefully: the effective interval stretches to the server's pace. ## What the dropped values mean Because ignored values are discarded, `exhaustMap` is wrong whenever **every** value matters: - saving each edit — dropped edits are lost; use `concatMap`; - a search box — newer queries are ignored while an old one runs; use `switchMap`; - a value that must be acted on after the current work — `exhaustMap` has no memory of it. ## Completion and errors - The output completes only when the source has completed **and** the active inner has finished, so if the source completes while a login request is in flight, that request's result is still delivered before the output completes. - An error from the inner observable errors the whole output. For a login stream that must accept another attempt after a failed one, the inner request has to turn its error into a value (a failed-login result) so the outer stream survives. - For polling, an unhandled error ends the poll entirely; the next tick never starts a request. ## Related operators - `exhaustAll()` applies the same strategy to a stream of observables; it is `exhaustMap` with the identity function. The older name `exhaust` is deprecated in 7.8 and slated for removal in v8. - The `resultSelector` overload of `exhaustMap` is deprecated; use an inner `map`. ## Interview angle The question usually arrives as "a user double-clicks Submit and the order is placed twice — fix it with RxJS". The expected answer names `exhaustMap`, explains that it discards rather than queues, and contrasts it with `concatMap`, which would still submit twice.
- Why is concatMap wrong for a login button?It queues extra clicks instead of discarding them, so a double click still sends a second login request after the first completes. `exhaustMap` drops those clicks, so each attempt produces exactly one request.
- Why can switchMap-based polling never produce a result against a slow server?Each tick unsubscribes the running request and starts a new one. If a response always takes longer than the interval, every request is cancelled before it finishes, so nothing is emitted; `exhaustMap` lets the running request finish instead.
saying these in an interview costs you the question
- exhaustMap queues clicks and sends them once the current request finishes.
- exhaustMap cancels the running request when a new click arrives.
- Disabling the button makes the flattening operator irrelevant.
- exhaustMap is a good fit for saving every edit.
- Values ignored by exhaustMap are replayed when it becomes idle.