In RxJS, what is the difference between observeOn and subscribeOn, and which one changes when values reach the observer?
answer
- one moves the subscribe call
- one moves each notification
- errors are rescheduled too
- position in the chain matters
basics
~20 sRxJS subscribeOn schedules the moment the source is subscribed, so a synchronous source still emits in one burst later. observeOn reschedules every next, error and complete notification on the scheduler, so it changes when values reach downstream observers.
solid answer
~40 s`subscribeOn(scheduler)` wraps the `source.subscribe(...)` call in `scheduler.schedule`, so the **subscription** happens later on that scheduler; once it happens, the source emits however it normally does, and a synchronous source still delivers all its values in one go inside that scheduled task. `observeOn(scheduler)` inserts a proxy observer that reschedules **every notification**, `next`, `error` and `complete`, on the scheduler, so it controls when values arrive downstream of it. Neither replaces the source's own scheduler. Unlike `delay`, `observeOn` also delays errors. Both accept an optional delay as a second argument, zero by default. A typical use is `observeOn(animationFrameScheduler)` to render values just before a repaint; `subscribeOn` appears far less often in application code.
code
ts · 12 linesimport { asyncScheduler, merge, observeOn, of, subscribeOn } from 'rxjs';
// subscribeOn: the subscription of the first source waits for a timer.
merge(of(1, 2).pipe(subscribeOn(asyncScheduler)), of(3, 4)).subscribe((v) => console.log('sub', v));
// sub 3, sub 4, sub 1, sub 2
// observeOn: values arrive after the current synchronous code.
of('a', 'b')
.pipe(observeOn(asyncScheduler))
.subscribe((v) => console.log('obs', v));
console.log('after subscribe');
// after subscribe, obs a, obs bgo deeper
Recall that subscribeOn is about when the subscription starts and observeOn is about when values are delivered.
Explain the mechanics: subscribeOn schedules source.subscribe once, observeOn reschedules every notification including errors, and neither changes the source's own scheduler.
Use observeOn deliberately, for example to render on animation frames, and prefer passing a scheduler to the source when the producer's own timing must change.
Question whether explicit scheduling belongs in application code at all, and keep it at rendering or interop boundaries where timing is a real requirement.
## Two different moments Every RxJS stream has two moments a scheduler could influence: - the moment the source is **subscribed**, which is when a cold observable's producer starts; - the moments its **notifications** (`next`, `error`, `complete`) are delivered to the observer. `subscribeOn` moves the first. `observeOn` moves the second. ## subscribeOn In RxJS 7.8 the implementation is one line: the operator schedules `source.subscribe(subscriber)` on the given scheduler and adds the scheduled action to the subscriber so it can be cancelled. Consequences: 1. The source's producer starts later, on the chosen scheduler. 2. The source's **emissions are not rescheduled**. A synchronous source such as `of(1, 2, 3)` still emits all three values back to back, just inside the scheduled task. 3. Unsubscribing before the scheduled task runs means the source is never subscribed at all. The RxJS docs illustrate it with `merge`: `merge(of(1, 2, 3).pipe(subscribeOn(asyncScheduler)), of(4, 5, 6))` logs `4, 5, 6, 1, 2, 3`, because the second source subscribes and emits synchronously while the first waits for a timer. ## observeOn `observeOn` subscribes to its source immediately, but every notification it receives is re-emitted through `scheduler.schedule(...)`: - **`next`** values are delivered on the scheduler, one scheduled task each. - **`complete`** is rescheduled too, so it stays after the last value. - **`error`** is rescheduled as well. This is the key difference from `delay`, which shifts values but passes an error through immediately. The guide's example makes the effect visible: a synchronous observable piped through `observeOn(asyncScheduler)` logs `just before subscribe`, `just after subscribe`, and only then `got value 1, 2, 3` and `done`. ## Side by side | | `subscribeOn(s)` | `observeOn(s)` | |---|---|---| | What is scheduled | the call to `source.subscribe` | each `next`, `error`, `complete` | | Producer start time | moved onto `s` | unchanged | | Timing of each value | unchanged relative to the producer | moved onto `s` | | Effect on errors | none | errors delayed like values | | Affects which operators | the subscription of everything upstream of it | notifications seen by everything downstream of it | | Typical use | deferring a subscription side effect, tests | rendering on `animationFrameScheduler`, forcing async delivery | ## Neither replaces the source's scheduler A common misconception is that `observeOn(animationFrameScheduler)` makes an `interval(10)` run on animation frames. It does not: the interval still ticks on `asyncScheduler` timers, and `observeOn` then hands each tick to the next animation frame. The RxJS docs also warn against using `observeOn` to break a large synchronous burst into asynchronous chunks; to change how a source itself produces values, pass the scheduler to the creation function (for example `interval(period, scheduler)` or `scheduled(input, scheduler)`). ## Where the operator sits in the pipe Position matters differently for the two operators: - `observeOn` affects only what is **downstream** of it. Operators above it still run in the source's own timing; operators below it, and the observer, run on the new scheduler. Placing it just before `subscribe()` limits the change to delivery. - `subscribeOn` affects the **subscription** of everything upstream of it, because subscribing travels up the chain. Wherever it sits, the source is subscribed from inside the scheduled task, so its exact position is less significant than `observeOn`'s. ## A worked timeline With a synchronous source `of('a', 'b')`: 1. **No operator**: `a`, `b`, `complete` are delivered during the `subscribe()` call, before the next line of code runs. 2. **`subscribeOn(asyncScheduler)`**: `subscribe()` returns immediately; when the timer fires, the source is subscribed and `a`, `b`, `complete` are delivered back to back inside that one task. 3. **`observeOn(asyncScheduler)`**: the source emits `a`, `b`, `complete` synchronously into `observeOn`, which schedules three separate deliveries; the observer receives them later, in order. ## Choosing between them - To control **when downstream code runs**, for instance to render values just before a repaint, use **`observeOn`**. - To control **when a producer starts**, for instance to defer an expensive synchronous setup out of the current call stack, use **`subscribeOn`**. - To delay values by a period of time while letting errors through immediately, use **`delay`**, not `observeOn` with a delay argument. - Both take an optional second argument, a delay in the scheduler's time unit, defaulting to `0`.
- How does observeOn(asyncScheduler, 100) differ from delay(100)?Both shift `next` values by 100 ms, but `observeOn` also reschedules `error` and `complete` with the same delay, whereas `delay` forwards an error from the source immediately. The RxJS docs recommend `delay` for delaying values and `observeOn` for choosing the scheduler that delivers notifications.
- Does observeOn(animationFrameScheduler) make an interval(10) source tick once per animation frame?No. The interval still ticks on `asyncScheduler` timers roughly every 10 ms; `observeOn` only hands each tick to the next animation frame for delivery. To drive the source itself from frames, the scheduler must be given to the creation function.
saying these in an interview costs you the question
- subscribeOn makes a synchronous source emit each value on a separate tick.
- observeOn replaces the scheduler the source uses to produce values.
- observeOn passes errors through immediately, exactly like delay.
- observeOn and subscribeOn are interchangeable ways to make a stream asynchronous.
- observeOn affects the operators placed before it in the pipe.