skip to content

In RxJS, why does from(fetch(url)) not send a new request per subscription, and how does defer() change that?

level: middleimportance: should knowfreq 45%

answer

  1. fetch runs when the expression runs
  2. one promise, many subscribers
  3. a factory called per subscribe
  4. iif is built on defer

basics

~20 s

fetch(url) starts the request as soon as the expression is evaluated, and from() only adapts that one promise, so every subscriber gets the same response. defer(() => fetch(url)) calls the factory on each subscribe, sending one request per subscription and none before.

solid answer

~40 s

In `from(fetch(url))`, the `fetch` call runs when the line executes, before anyone subscribes, and `from` just wraps the single resulting promise. Every subscriber - and every resubscription, for example by a retry - reads that one settled result, and unsubscribing cannot cancel the request. `defer(factory)` stores a function instead and calls it **inside each subscribe**, converting whatever it returns with the same rules as `from`. So `defer(() => fetch(url))` sends nothing until subscribed and a fresh request per subscription. `iif(condition, a, b)` is `defer` underneath, so its condition is evaluated at subscribe time - but `a` and `b` are ordinary arguments, evaluated when `iif` is called. `ajax()` from `rxjs/ajax` is lazy by design and aborts its request on unsubscribe.

code

ts · 13 lines
ts
import { defer, from } from 'rxjs';

const eager$ = from(fetch('/api/config')); // request already sent here
const lazy$ = defer(() => fetch('/api/config')); // nothing sent yet

eager$.subscribe(); // no new request
eager$.subscribe(); // no new request: same promise

lazy$.subscribe(); // request 1
lazy$.subscribe(); // request 2

const stamp$ = defer(async () => ({ at: Date.now(), body: await (await fetch('/api/ping')).text() }));
// each subscription calls the async factory once

go deeper

for a junior

Recall that defer takes a function and calls it when someone subscribes, which makes promise-based work lazy.

for a middle

Explain why from(fetch()) sends one request shared by all subscribers, how defer gives one request per subscription, and that iif is defer underneath.

for a senior

Diagnose retries that never re-send, wasted iif branches and uncancellable requests, and choose between defer around a promise and an abortable source.

for a principal

Set a convention that service methods return lazy Observables, never pre-started promises, so callers control when work starts and whether it can be cancelled.

## The eager promise problem A Promise-returning function such as `fetch(url)` does its work when it is **called**. In the expression `from(fetch(url))`, JavaScript evaluates the argument first, so the HTTP request is already on its way before `from` runs and long before anyone subscribes. RxJS's `from()` then adapts that single promise into an Observable: - every subscriber receives the **same** response, because there is only one promise; - **re-subscribing** - which is what retry operators do - reads the same settled promise again, so a failed request is never actually re-sent; - **unsubscribing** stops the value being delivered but cannot cancel the request, because the promise has no cancel; - a request is made even if nothing ever subscribes. None of this is a bug in `from`; it faithfully adapts what it is given. The fix is to delay creating the promise. ## What defer does `defer(observableFactory)` returns an Observable whose subscribe logic is, in RxJS 7's source, a single line: call `observableFactory()`, convert the result with the same rules as `from()`, and subscribe to it. Because the factory runs **inside subscribe**: 1. nothing happens until someone subscribes; 2. each subscription calls the factory again and gets its own fresh source; 3. resubscribing after an error runs the factory again, so a retry really does send a new request. The factory may return any `ObservableInput` - an Observable, a promise, an array, an iterable - so `defer(async () => { ... })` also works: each subscription calls the async function once. | Expression | Request starts | Requests for 3 subscribers | Retry re-sends | |---|---|---|---| | `from(fetch(url))` | when the line runs | 1 | no | | `defer(() => fetch(url))` | on each subscribe | 3 | yes | | `ajax(url)` (`rxjs/ajax`) | on each subscribe | 3 | yes | ## Other uses of defer The same pattern fixes anything that captures state too early: - **Timestamps or random values**: `of(Date.now())` captures the time when the line runs; `defer(() => of(Date.now()))` captures it per subscription. - **Reading storage or configuration**: `defer(() => of(localStorage.getItem(key)))` reads the current value each time instead of once at module load. - **Choosing a source at subscribe time**: pick one of several Observables based on current state. ## iif: a conditional defer `iif(condition, trueResult, falseResult)` is implemented as `defer(() => condition() ? trueResult : falseResult)`. Two consequences: - the **condition function** is evaluated per subscription, not when `iif` is called; - the **two results** are ordinary function arguments, evaluated when `iif` is called. If they are Observables, that is harmless - they are lazy. If they are promises, as in `iif(() => online, fetch(a), fetch(b))`, **both requests start immediately**, and the condition only chooses which result to read. When the branches have side effects, write `defer(() => online ? fetch(a) : fetch(b))` instead. ## ajax: a lazy, cancellable request source RxJS ships its own request function in the `rxjs/ajax` entry point. `ajax(url)` and `ajax.getJSON(url)` build an Observable that creates an `XMLHttpRequest` **on each subscribe** and, in its teardown, calls `xhr.abort()` if the request has not finished. A status of 400 or above arrives as an `AjaxError`. In Angular applications, `HttpClient` is the usual request API instead, and it is covered on its own. ## defer versus a function that returns an Observable A common alternative is a plain function, `load = () => from(fetch(url))`. It is lazy **per call**: each `load()` call starts a request, and whoever calls it decides when. `defer` moves that decision to **subscribe** time, which matters when the Observable itself is passed around: - a single `config$ = defer(...)` can be stored in a field and subscribed many times, each time with a fresh request; - operators that resubscribe, such as retry and repeat, re-run the work; - nothing runs if the stream is built but never subscribed, for example when a condition later skips it. ## How to spot the bug in review - A Promise-returning call written directly inside `from(...)` at module or service-construction level. - `iif` whose branches are promises or function calls with side effects. - A retry around a promise-backed Observable that "never retries" in the network log. The remedy in each case is the same: move the call into a factory with `defer`, or use a source that is already lazy.

  • In RxJS, what is wrong with iif(() => isOnline, fetch('/a'), fetch('/b'))?
    Both `fetch` calls run when `iif` is called, because they are ordinary arguments; the condition only decides which settled result the subscriber reads. The unused request is wasted and may have side effects. Wrap the choice in `defer(() => isOnline ? fetch('/a') : fetch('/b'))` so only the chosen request is sent, at subscribe time.
  • In RxJS, does unsubscribing from from(fetch(url)) cancel the HTTP request?
    No. The Observable adapts a promise that is already running, and a promise has no cancel, so unsubscribing only stops the value reaching the subscriber. Cancelling needs a source whose teardown aborts the request, such as `ajax()` from `rxjs/ajax`, which calls `xhr.abort()` on unsubscribe if the request has not finished.

from(fetch(url)) is a photocopy of a letter already posted: everyone gets a copy of the same reply. defer(() => fetch(url)) is a stamped, addressed envelope handed to each subscriber, who posts their own letter when they subscribe.

saying these in an interview costs you the question

  • from(fetch(url)) waits for a subscriber before sending the request.
  • Retrying a from(promise) Observable sends the request again.
  • iif only evaluates the argument expression of the branch the condition selects.
  • defer's factory runs once and its result is shared by every subscriber.
  • Unsubscribing from from(fetch(url)) aborts the HTTP request.