In RxJS, when exactly does the teardown returned from a new Observable subscriber function run, and what happens to next() calls afterwards?
answer
- three ways a subscription ends
- observer callback first, cleanup second
- a stopped Subscriber drops notifications
- add() on a closed subscription
basics
~20 sRxJS runs the teardown once, on whichever comes first: unsubscribe(), error() or complete(); on error or complete it runs after the observer's callback. Afterwards the Subscriber is stopped, and later next, error or complete calls are silently ignored.
solid answer
~40 sThe teardown runs **once**, when the subscription ends by any of three routes: the consumer calls `unsubscribe()`, the producer calls `subscriber.error()`, or it calls `subscriber.complete()`. On the two terminal routes the `Subscriber` first delivers the notification to the observer, then unsubscribes itself, which runs the teardown. `unsubscribe()` is idempotent, so a second call does nothing. Once stopped, the `Subscriber` ignores further `next`, `error` and `complete` calls; by default they are no-ops, and `config.onStoppedNotification` can be set to observe them. A producer looping synchronously should check `subscriber.closed` to stop early. If the function completes synchronously before returning its teardown, RxJS runs that teardown immediately when it is added.
code
ts · 23 linesimport { Observable } from 'rxjs';
const ticks$ = new Observable<number>((subscriber) => {
let n = 0;
const id = setInterval(() => {
subscriber.next(n++);
if (n === 3) {
subscriber.complete();
}
}, 100);
return () => {
console.log('teardown');
clearInterval(id);
};
});
const sub = ticks$.subscribe({
next: (v) => console.log('next', v),
complete: () => console.log('complete'),
});
// next 0, next 1, next 2, complete, teardown
setTimeout(() => sub.unsubscribe(), 1000); // no second 'teardown'go deeper
Recall that the function returned from new Observable is the cleanup, and that RxJS calls it when the subscription ends.
Explain the three endings, the order of observer callback then teardown on error and complete, idempotent unsubscribe, and that late next() calls are ignored.
Diagnose leaky custom producers: late notifications are dropped but the work continues, synchronous loops need subscriber.closed, and onStoppedNotification can expose the culprit in development.
Argue for a team rule that every custom Observable owns its cleanup in teardown, reviewed like resource handling in any other language, instead of relying on consumers to remember.
## Three ways a subscription ends An RxJS subscription ends in exactly one of three ways, and the **teardown** - the function (or `Subscription`, or object with `unsubscribe()`) that the subscriber function returned - runs on all of them: | Ending | Who triggers it | Observer callback called | Teardown runs | |---|---|---|---| | `unsubscribe()` | the consumer | none | yes, immediately | | `subscriber.error(err)` | the producer | `error` | yes, after the callback | | `subscriber.complete()` | the producer | `complete` | yes, after the callback | The detail people miss is the terminal routes. In RxJS 7's `Subscriber`, `error()` and `complete()` first set an internal stopped flag, then forward the notification to the observer inside a `try`, and in the `finally` block call `this.unsubscribe()`. So the observer sees `complete` **before** the cleanup runs, and a producer never needs to clean up by hand before signalling the end. ## Exactly once `Subscription.unsubscribe()` checks its `closed` flag. The first call sets it and runs the finalizers; every later call returns without doing anything. That is why a consumer can safely call `unsubscribe()` on a subscription that already completed - the teardown will not run a second time. If a finalizer throws, RxJS keeps running the remaining finalizers, collects the failures, and throws a single `UnsubscriptionError` at the end, so one broken cleanup does not skip the others. ## Notifications after the end are dropped After `error`, `complete` or `unsubscribe`, the `Subscriber` is **stopped**. If the producer keeps calling `next()` - say a timer it forgot to clear - those calls do not reach the observer: - by default they are silent no-ops; - if `config.onStoppedNotification` is set (import `config` from `'rxjs'`), RxJS calls it asynchronously with the dropped notification, which is useful for spotting leaky producers in development. The drop protects the consumer but not the resource: a timer that was never cleared still fires. The contract's guarantee is only that the observer is not called; stopping the work is the teardown's job. ## The synchronous edge cases Two situations surprise people because the subscriber function runs synchronously inside `subscribe()`: 1. **Completing before returning the teardown.** RxJS attaches the returned teardown with `subscriber.add(...)` after the function returns. If the function has already called `complete()`, the subscriber is closed by then, and `Subscription.add()` on a closed subscription **executes the finalizer immediately**. The cleanup still happens; it just happens at the end of `subscribe()`. 2. **A synchronous producer and an early unsubscribe.** A loop such as `for (let i = 0; i < 1_000_000; i++) subscriber.next(i)` keeps running even if a downstream `take(1)` has already unsubscribed, because the loop never yields. Checking `subscriber.closed` in the loop condition lets it stop as soon as nobody is listening. A related rule: if the subscriber function **throws synchronously**, RxJS catches the exception and delivers it as an `error` notification, so it ends the subscription like any other error rather than escaping from `subscribe()`. ## Unsubscribe is not complete From the consumer's side, the observer's `complete` callback fires only when the **producer** finishes. When the consumer calls `unsubscribe()`, no observer callback fires at all - the teardown runs, and that is the whole event. Code that puts cleanup in the consumer's `complete` callback therefore misses the most common ending in UI code, where screens unsubscribe long before a stream would complete. Cleanup that must happen on every ending belongs in the producer's teardown. ## Registering several resources Because `Subscriber` extends `Subscription`, a producer can call `subscriber.add(teardown)` as it acquires each resource instead of returning one combined function. In RxJS 7 `add()` returns `void` - the v6 behaviour of returning a `Subscription` was removed as a breaking change - and it accepts functions, `Subscription` objects or anything with `unsubscribe()`. ## What to say in the interview - The teardown runs on unsubscribe, error and complete - not only on unsubscribe. - On error and complete, the observer's callback runs first and the teardown second. - It runs once; repeated `unsubscribe()` calls are harmless. - Late notifications are ignored, but a producer must still stop its own work, and `subscriber.closed` is how a synchronous producer notices. These points separate someone who has written custom Observables from someone who has only consumed them.
- In RxJS, why should a synchronous producer loop check subscriber.closed?Because the loop never yields, an operator such as `take(1)` downstream can unsubscribe after the first value while the loop keeps calling `next()` a million times. The calls are dropped, but the work still runs. Checking `!subscriber.closed` in the loop condition lets the producer stop as soon as the consumer has gone.
- In RxJS 7, what does Subscription.add() return, and what happens if the subscription is already closed?It returns `void`; returning a `Subscription` was RxJS 6 behaviour and was removed in 7. If the target subscription is already closed, `add()` executes the finalizer immediately rather than storing it, so late-registered cleanup still runs. Adding a subscription to itself, or adding `null`, does nothing.
saying these in an interview costs you the question
- Teardown only runs when the consumer explicitly calls unsubscribe().
- On complete, RxJS runs the teardown before the observer's complete callback.
- Calling unsubscribe() twice runs the teardown twice.
- next() after complete() still reaches the observer if the teardown has not run yet.
- Dropping late notifications means the producer's timer or listener is stopped automatically.
- An exception thrown inside the subscriber function escapes from subscribe() to the caller.