skip to content

In RxJS, how do you end several subscriptions at once with a parent Subscription, and how does Subscription.add() behave?

level: middleimportance: should knowfreq 45%

answer

  1. one handle for many
  2. add() accepts functions too
  3. a closed parent runs it now
  4. children remove themselves
  5. void return since version 7

basics

~20 s

Create one RxJS Subscription, add() each child subscription or teardown function to it, and call unsubscribe() once. add() returns void in RxJS 7, runs a teardown immediately if the parent is already closed, and closed children drop out automatically.

solid answer

~40 s

Create a parent with `new Subscription()`, call `parent.add(child)` for every subscription (or plain teardown function) the owner creates, and call `parent.unsubscribe()` once when the owner is destroyed; every child is torn down with it. A few details matter. In RxJS 7 `add()` returns `void`, so older code that chained on its return value no longer type-checks. If the parent is already closed, `add()` executes the teardown immediately, so a parent is single-use: reuse it after `unsubscribe()` and new children die instantly. Adding an already-closed subscription, the same subscription twice, or the parent to itself is a no-op. When a child completes or is unsubscribed on its own, it removes itself from the parent, so the list does not grow with finished work. `remove()` detaches a teardown without running it.

code

ts · 22 lines
ts
import { Subscription, interval } from 'rxjs';

class Poller {
  private subs = new Subscription();

  start(): void {
    // A closed parent would tear these down immediately, so start fresh.
    if (this.subs.closed) {
      this.subs = new Subscription();
    }
    this.subs.add(interval(5000).subscribe(() => this.refresh()));
    this.subs.add(() => console.log('poller stopped'));
  }

  stop(): void {
    this.subs.unsubscribe(); // ends every child and runs the function
  }

  private refresh(): void {
    /* ... */
  }
}

go deeper

for a junior

Recall that new Subscription() plus add() lets one unsubscribe() call end many subscriptions and teardown functions.

for a middle

Explain add()'s edge rules: void return in version 7, immediate execution on a closed parent, no-ops for closed or duplicate children, self-removal on completion.

for a senior

Diagnose restart bugs where a reused closed parent kills new subscriptions instantly, and choose between a parent, takeUntil and framework teardown per owner.

for a principal

Decide how teams standardise teardown so reviews can check it mechanically, and weigh explicit handles against notifier-based patterns for readability.

## The problem a parent Subscription solves An object that owns several RxJS subscriptions, such as a view, a widget or a service with a lifetime, must release all of them when it is destroyed. Keeping each handle in its own field and calling `unsubscribe()` on each is error-prone: it is easy to forget one. A **parent `Subscription`** groups them so a single `unsubscribe()` call ends everything. ```ts import { Subscription, interval, fromEvent } from 'rxjs'; const subs = new Subscription(); subs.add(interval(1000).subscribe(tick)); subs.add(fromEvent(window, 'resize').subscribe(relayout)); subs.add(() => console.log('widget torn down')); // later, when the owner is destroyed subs.unsubscribe(); ``` ## What add() accepts `add()` takes a **teardown**, which in RxJS 7 can be: - another `Subscription` (the usual case), - a plain function, called when the parent unsubscribes, - any object with an `unsubscribe()` method. The parent keeps a list of these **finalizers** and runs them all, in order, when it is unsubscribed. `remove(teardown)` takes one back out without running it. ## The rules of add(), verified against RxJS 7.8 | Situation | What `add()` does | |---|---| | parent open, child open | stores the child and records the parent on the child | | parent already closed | runs the teardown **immediately** | | child already closed | nothing (no-op) | | same child added twice | nothing the second time | | `null`, `undefined` or the parent itself | nothing | Two consequences follow: 1. **A parent is single-use.** Once `subs.unsubscribe()` has run, `subs.closed` is `true` forever. Any subscription added afterwards is torn down the moment it is added. Code that re-initialises an owner must create a **new** `Subscription`, not reuse the old field. 2. **Finished children do not accumulate.** When a child completes, errors or is unsubscribed directly, it removes itself from every parent it was added to. A parent that collects hundreds of short-lived request subscriptions over a long session does not grow without bound, which is the main advantage over pushing handles into an array. ## What happens when the parent unsubscribes When `subs.unsubscribe()` runs, RxJS: 1. marks the parent **closed**, so later calls do nothing; 2. removes the parent from any parents it was itself added to, so nesting parents works; 3. runs every finalizer in the order it was added: child subscriptions are unsubscribed (which tears down their whole operator chains), functions are called, `Unsubscribable` objects get `unsubscribe()`. If a finalizer throws, the remaining finalizers **still run**; RxJS collects the failures and throws a single `UnsubscriptionError` at the end. One faulty cleanup therefore cannot leave the other subscriptions running, but the error still surfaces to whoever called `unsubscribe()`. ## The version 7 change: add() returns void In RxJS 6, `add()` returned a `Subscription`, and some code chained on that value or stored it for a later `remove()`. RxJS 7 changed the signature to return **`void`** to remove that confusing legacy behaviour, and at the same time let `remove()` accept plain functions as well as subscriptions. Code written against version 6 that uses the return value fails to type-check after the upgrade; the fix is to keep a reference to the child you added. ## Parent Subscription versus the alternatives - **An array of handles** works but keeps finished subscriptions in memory and needs a loop to clean up. - **`takeUntil(destroy$)`** on each stream avoids storing handles, but must be placed last in every pipe and depends on a notifier that actually emits. - **A parent Subscription** is explicit, needs no extra subject, and unsubscribing it travels up each child's whole operator chain, tearing down inner subscriptions as well. Many codebases mix them: `takeUntil` for streams declared in a pipeline, a parent `Subscription` for imperative `subscribe()` calls and teardown functions such as removing a third-party widget. ## Common mistakes - Storing `subs.add(...)`'s return value, which is `undefined` in RxJS 7. - Reusing a closed parent after the owner is re-created. - Assuming a function added to an already-closed parent is silently ignored; it runs straight away. - Adding the subscription before the stream has been subscribed, for example adding the observable instead of the `Subscription` returned by `subscribe()`.

  • Why is a parent Subscription preferable to pushing handles into an array for a long-lived owner?
    A child that completes or is unsubscribed removes itself from its parents, so a parent only holds work that is still running. An array keeps every handle ever pushed, including finished ones, until the owner clears it, which grows memory in a long session and needs a loop to clean up.
  • What does parent.remove(teardown) do, and does it run the teardown?
    It detaches a previously added teardown without executing it, so the parent will no longer run it on unsubscribe. In RxJS 7 it accepts functions as well as subscriptions. If the same function was added twice, it must be removed twice.

saying these in an interview costs you the question

  • Subscription.add() returns the child so calls can be chained.
  • Adding a live subscription to an already-unsubscribed parent leaves it running.
  • Completed children stay in the parent's list until the parent is unsubscribed.
  • A parent Subscription can be reused after unsubscribe() for the next batch.
  • Only Subscription objects can be added, not plain teardown functions.