In an Angular component, which subscriptions end on their own and which need teardown: HttpClient calls, `Router.events`, a FormControl's `valueChanges`, and RxJS `interval()`?
answer
- does the source ever complete
- one response, then complete
- the Router outlives every component
- who owns the control
- timers never stop themselves
basics
~10 sIn Angular, HttpClient calls complete after the response, but Router.events, valueChanges and RxJS interval() never complete, so their subscriptions outlive the component unless tied to DestroyRef, the async pipe or toSignal.
solid answer
~40 sThe deciding question is whether the source **completes**. An `HttpClient` call emits its response (or progress events) and completes, so its subscription ends without help, although its callback can still run after the component is destroyed if the response arrives late. `Router.events` is a `Subject` on the root `Router` that never completes while the app runs, so a component subscription to it outlives the component. `valueChanges` is an `EventEmitter` that never completes; it keeps running for as long as the control lives, and it becomes a real leak when the control is owned by something that outlives the component. RxJS `interval()` never completes and its timer keeps the subscriber alive. So tie every non-completing subscription to the component with `takeUntilDestroyed`, or let the async pipe or `toSignal()` own it.
code
ts · 19 linesimport { Component, inject, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Router } from '@angular/router';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { interval } from 'rxjs';
@Component({ selector: 'app-status-bar', template: `{{ tick() }}` })
export class StatusBar {
readonly tick = signal(0);
constructor() {
// Completes after the response: no leak, but late callbacks are still possible.
inject(HttpClient).get('/api/status').subscribe();
// Never complete: tie them to this component's DestroyRef.
inject(Router).events.pipe(takeUntilDestroyed()).subscribe();
interval(5000).pipe(takeUntilDestroyed()).subscribe(() => this.tick.update(n => n + 1));
}
}go deeper
Remember that HttpClient calls complete on their own, while Router.events, valueChanges and interval() need to be ended when the component is destroyed.
Explain completion as the test, why the root Router outlives components, and how control ownership decides whether valueChanges leaks.
Audit components for non-completing subscriptions, explain late HTTP callbacks and aborts, and justify a uniform teardown rule over case-by-case reasoning.
Choose and enforce a teardown policy, weighing lint rules and code review against the cost of rare but hard-to-trace leaks.
## The one question to ask: does it complete? A subscription is released when the source **completes**, **errors**, or when someone **unsubscribes**. If the source never completes and nobody unsubscribes, the subscription lives as long as the source does, and the callback keeps a reference to the component that created it. ## The four usual suspects | Source | Completes? | Who keeps it alive | Needs teardown? | |---|---|---|---| | `http.get(...)` from `HttpClient` | yes, after the response | nothing once done | not for memory; optional to stop a late callback | | `Router.events` | no | the root `Router`, for the app's lifetime | **yes** | | `form.valueChanges` / `statusChanges` | no | whatever owns the control | **yes** when the control outlives the component; good practice always | | RxJS `interval(1000)` | no | the scheduler's timer | **yes** | ### HttpClient An `HttpClient` Observable dispatches one request per subscription, emits the response (or the progress events you asked for) and **completes**. Angular's HTTP guide says that, because of this automatic completion, there is usually no memory-leak risk, but it still recommends cleaning up: if the component is destroyed before the response, the callback runs anyway and may touch state that no longer matters. Unsubscribing also **aborts** the in-flight request. ### Router.events `Router.events` is backed by a `Subject` on the **root** `Router` service. It never completes during normal operation; the router only releases its observers when the router itself is disposed. A component that subscribes without teardown adds a subscriber that survives every navigation. Navigate back and forth ten times and ten callbacks run on every router event. ### valueChanges A `FormControl`'s `valueChanges` is an `EventEmitter` created with the control, and it **never completes**. What happens after the component is destroyed depends on who owns the control: - If the component **created** the control and nothing else references it, the control, the subscription and the component become unreachable together and are collected; nothing keeps firing. - If the control is **owned elsewhere** (a parent's form, a form held in a service, a control passed in through an input), it outlives the component, and every destroyed instance leaves a subscriber behind that still runs. Because ownership changes during refactoring, most teams tie `valueChanges` subscriptions to `DestroyRef` regardless. ### interval and timers `interval()` schedules a repeating timer that holds its subscriber. It never completes, and the timer is owned by the browser, not by the component, so the callback runs every tick for the lifetime of the page unless the subscription is ended. ## How to end them 1. **`takeUntilDestroyed()`** in the constructor, or `takeUntilDestroyed(this.destroyRef)` elsewhere, placed last in the pipe. 2. **The async pipe** when the value is only used in the template. 3. **`toSignal()`**, which unsubscribes when its injection context is destroyed. 4. A **completing operator** (`take(1)`, `first()`) when only the first value matters, noting that it does not end the subscription on destroy if the value has not arrived yet. ## What interviewers listen for - The test is **completion**, not whether the API "is Angular's". - **HttpClient completes**; `Router.events`, `valueChanges` and `interval` **do not**. - `valueChanges` leaks when the **control outlives** the component. - The standard tools: **`takeUntilDestroyed`**, the **async pipe**, **`toSignal()`**.
- If an HttpClient call completes by itself, why tie it to DestroyRef at all?Completion prevents a permanent leak, but if the user leaves before the response arrives the callback still runs against a destroyed component, perhaps navigating or showing a toast. Ending the subscription on destroy also aborts the in-flight request, saving the round trip.
- Does take(1) make a Router.events subscription safe?Only once a value arrives. `take(1)` completes after the first matching event, but if the component is destroyed before any such event, the subscription stays registered on the root Router. `takeUntilDestroyed` covers that case too.
saying these in an interview costs you the question
- Every Observable returned by an Angular API completes automatically
- HttpClient subscriptions leak memory unless you unsubscribe
- Router.events completes when the component that subscribed is destroyed
- valueChanges completes when the component is destroyed
- take(1) is always equivalent to takeUntilDestroyed