skip to content

Unsubscribing & Schedulers

Schedulers such as asyncScheduler and animationFrameScheduler decide when work runs; unsubscribe, takeUntil and take(1) decide when it stops. Interviewers probe subscription leaks first.

part ofRxJSoverview, primer and where to startread it →
on this pageshow

explore

questions

6

In RxJS, when must you call unsubscribe() on a Subscription, and when does a subscription end on its own?

level: juniorimportance: must knowfreq 78%

answer

  1. terminal notifications end the work
  2. complete or error runs teardown
  3. some sources never finish
  4. one value, then done
  5. keep the handle subscribe() returns

basics

~10 s

An RxJS subscription ends on its own when the source completes or errors, which runs its teardown. Sources that never finish, such as interval, fromEvent or a long-lived Subject, need unsubscribe(), take(1) or takeUntil.

solid answer

~40 s

`subscribe()` returns a `Subscription`, and RxJS tears it down automatically after a `complete` or `error` notification: the subscriber unsubscribes itself and every finalizer runs. So a stream that emits and completes, like `of(x)` or a typical one-shot request observable, cleans itself up. Sources that never complete, such as `interval`, `fromEvent`, or a `Subject` owned by a long-lived service, keep the observer and everything its closure captures alive until someone unsubscribes. For those, either keep the `Subscription` and call `unsubscribe()` when the owner goes away, or bound the stream with `take(1)` or `takeUntil(notifier)`. Calling `unsubscribe()` early is always safe: it is idempotent and runs the source's teardown, which clears a pending timer or aborts a request when the source supports it.

code

ts · 15 lines
ts
import { interval, of, take } from 'rxjs';

// Ends on its own: of() completes after the last value.
const finite = of(1, 2, 3).subscribe((n) => console.log(n));
console.log(finite.closed); // true

// Never ends on its own: must be released.
const ticking = interval(1000).subscribe((n) => console.log('tick', n));
setTimeout(() => ticking.unsubscribe(), 3500); // clears the timer

// Bounded: completes after the first value.
interval(1000).pipe(take(1)).subscribe({
  next: (n) => console.log('first', n),
  complete: () => console.log('done, already unsubscribed'),
});

go deeper

for a junior

Recall that complete and error end a subscription automatically, and name the everyday sources that never end: interval, fromEvent and long-lived subjects.

for a middle

Explain that the subscriber unsubscribes itself after a terminal notification, and when to reach for take(1), takeUntil or a stored Subscription.

for a senior

Show you can spot leaks in review: silent-but-open streams, take(1) on sources that may never emit, and cleanup placed in complete instead of finalize.

for a principal

Argue for a team convention, such as framework-bound teardown by default and manual subscriptions flagged in review, instead of case-by-case judgement.

## What a Subscription is In RxJS, calling `subscribe()` on an `Observable` starts one **execution** of that observable for one observer and returns a **`Subscription`**: a handle whose `unsubscribe()` method stops that execution and runs its **teardown** (also called finalizers). Teardown is whatever the source registered to release its resources: clearing a timer, removing a DOM event listener, closing a socket, aborting a request. The question "do I need to unsubscribe?" is really "will this execution end by itself, and soon enough?" ## When a subscription ends on its own An execution ends without your help in two ways, both **terminal notifications**: - **`complete`**: the producer says it has no more values. RxJS delivers `complete` to the observer, then the subscriber calls its own `unsubscribe()`, so all teardown runs. - **`error`**: the producer fails. RxJS delivers `error` to the observer, then unsubscribes the same way. After either one the `Subscription` is **closed** (`subscription.closed === true`). Calling `unsubscribe()` again is harmless because it is **idempotent**: a closed subscription ignores the call. This is why a stream that emits a finite set of values and completes does not leak. `of(1, 2, 3)`, `from([...])`, `timer(500)` without a period, and a typical one-shot request observable all finish and release their observer. ## Sources that never end The danger is a source that has no natural end. It keeps a reference to your observer, and your observer's closure keeps references to whatever it touches: a component instance, DOM nodes, large arrays. | Source | Completes by itself? | Needs explicit cleanup? | |---|---|---| | `of(...)`, `from(array)` | yes, after the last value | no | | `timer(500)` (no period) | yes, after one value | no, unless the owner may go away first | | `interval(1000)`, `timer(0, 1000)` | never | yes | | `fromEvent(el, 'click')` | never | yes | | a `Subject` or `BehaviorSubject` in a long-lived service | only if someone calls `complete()` | yes | A source that has merely gone quiet has **not** completed. A stream that emitted its last value an hour ago still holds its observer until `complete`, `error` or `unsubscribe()` happens. ## Ways to end it 1. **Keep the handle and call `unsubscribe()`** when the owning object is destroyed. This is the most direct option and it tears down the whole operator chain above it. 2. **`take(1)`** (or `take(n)`) makes the stream finite: after the first value it completes, which unsubscribes the source. Use it for one-shot reads of a stream that is known to emit. 3. **`takeUntil(notifier$)`** completes the stream when `notifier$` emits, for example a `destroy$` subject that the owner fires when it is torn down. It must normally be the last operator in the pipe. 4. **A parent `Subscription`**: collect several subscriptions with `parent.add(child)` and end them all with one `parent.unsubscribe()`. ## Choosing between the options - Prefer **making the stream finite** (`take`, `takeUntil` and similar operators) when the end condition is part of the stream's meaning, such as "the first saved value" or "until the dialog closes". - Prefer **keeping the handle** when the end is dictated by an owner's lifetime and the code is imperative, such as a service method that starts and stops a background refresh. - Prefer a **framework-provided binding** when one exists, because it removes the manual step that people forget. - Whatever you choose, make the end visible in the code: a reviewer should be able to point at the line that ends every long-lived subscription. ## Pitfalls interviewers probe - **`take(1)` only ends once a value arrives.** On a click stream that is never clicked, the subscription stays open until something else unsubscribes it. - **Unsubscribing is not completing.** `unsubscribe()` runs teardown but does not call the observer's `complete` callback, so cleanup written only inside `complete` is skipped. `finalize()` runs on completion, error and unsubscription alike. - **Garbage collection does not rescue you.** A running `interval` is referenced by the runtime's timer, which references the subscriber, which references your callback; nothing becomes collectable until teardown clears the timer. - **Synchronous sources finish before `subscribe()` returns.** For `of(1, 2, 3)` the returned subscription is already closed, which is fine. ## Where this sits in an Angular app In Angular code the same rules apply to subscriptions made in components and services. Framework helpers such as the async pipe and destroy-bound operators exist so that you rarely write the `unsubscribe()` call by hand, but they are built on exactly this contract: complete, error or unsubscribe are the only three ways an execution ends.

  • Is it harmful to call unsubscribe() on a subscription that has already completed?
    No. `unsubscribe()` is idempotent: the `Subscription` has a `closed` flag, and after `complete` or `error` it is already closed, so a later call does nothing. That is why cleanup code can unsubscribe unconditionally without checking whether the stream finished first.
  • Does take(1) guarantee that the subscription is released promptly?
    Only once a value arrives. `take(1)` completes after the first `next`, but until the source emits, the subscription stays open. On a stream that may never emit, such as a click that never happens, combine it with `takeUntil` or keep the `Subscription` and unsubscribe when the owner goes away.

saying these in an interview costs you the question

  • A stream that has stopped emitting has completed and released its observer.
  • Unsubscribing calls the observer's complete callback.
  • take(1) releases the subscription immediately even if nothing is ever emitted.
  • Garbage collection frees an interval subscription once its component is gone.
  • A subscription that errored still has to be unsubscribed to run its teardown.
open as a page

In RxJS, a view tears down with takeUntil(destroy$), yet its polling keeps firing after it closes; why does takeUntil's position in the pipe matter?

level: seniorimportance: must knowfreq 55%

basics

~20 s

RxJS takeUntil completes only what is downstream of it and unsubscribes only its upstream. A flattening or combining operator placed after it waits for its still-active inner or other sources, so polling survives; put takeUntil last.

open as a page

In RxJS, how do asyncScheduler, asapScheduler and queueScheduler differ in when they run a task scheduled with zero delay?

level: middleimportance: should knowfreq 35%

basics

~20 s

With zero delay, RxJS queueScheduler runs the task synchronously (queuing nested tasks until the current one ends), asapScheduler runs it as a microtask after the current code, and asyncScheduler runs it on a timer, after pending microtasks.

open as a page

In RxJS, what is the difference between observeOn and subscribeOn, and which one changes when values reach the observer?

level: middleimportance: should knowfreq 28%

basics

~20 s

RxJS 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.

open as a page

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%

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.

open as a page

In RxJS, how would you drive a smooth countdown animation with animationFrameScheduler, and why does interval(16, animationFrameScheduler) not stay in step with frames?

level: seniorimportance: nice to knowfreq 18%

basics

~20 s

Use interval(0, animationFrameScheduler) or animationFrames() to emit once per frame, compute remaining time from a clock, and end with takeWhile. A positive delay makes animationFrameScheduler fall back to a timer, so interval(16, ...) is not frame-aligned.

open as a page