skip to content

Observable-Returning APIs

HttpClient, the router, reactive forms and EventEmitter expose Observables that differ in being cold, completing or never ending. Interviewers ask which ones must be unsubscribed.

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

explore

questions

5

In Angular, why does calling HttpClient.get() without subscribing send no request, and why does subscribing to the result twice send two?

level: juniorimportance: must knowfreq 62%

answer

  1. the call builds, subscribe runs
  2. interceptors rerun per subscription
  3. one body, then complete
  4. unsubscribe aborts the request
  5. sharing needs an operator

basics

~20 s

HttpClient methods return cold Observables: the call only describes the request, and each subscription runs the interceptor chain and backend again, sending a new request. With the default observe: 'body' each run emits one body and completes.

solid answer

~40 s

`HttpClient.get()` returns a **cold** Observable built as `of(request)` piped into the handler chain, so the interceptors and the backend only run when someone subscribes. No `subscribe()`, no async pipe, no `toSignal()` means no request. Every subscription runs the chain again, so two subscriptions — two `async` pipes on the same Observable, or a `subscribe()` plus an async pipe — send two requests, and a `retry` re-runs the interceptors too. With the default `observe: 'body'`, each run emits the response body once and completes; a failure arrives as an `HttpErrorResponse` error. Unsubscribing before the response aborts the request; in Angular 22 the default backend is `FetchBackend`, which aborts through an `AbortController`. To share one response, you add a multicasting operator or cache it in a service.

code

ts · 29 lines
ts
import { Component, inject } from '@angular/core';
import { AsyncPipe } from '@angular/common';
import { HttpClient } from '@angular/common/http';

interface Product {
  id: string;
  name: string;
  price: number;
}

@Component({
  selector: 'app-product-summary',
  imports: [AsyncPipe],
  template: `
    <!-- Two async pipes = two subscriptions = two GET requests -->
    <h1>{{ (product$ | async)?.name }}</h1>
    <p>{{ (product$ | async)?.price }}</p>

    <!-- One subscription, reused inside the block -->
    @if (product$ | async; as product) {
      <h1>{{ product.name }}</h1>
      <p>{{ product.price }}</p>
    }
  `,
})
export class ProductSummary {
  private readonly http = inject(HttpClient);
  readonly product$ = this.http.get<Product>('/api/products/42');
}

go deeper

for a junior

Recall that nothing is sent until something subscribes, and that each subscription sends its own request.

for a middle

Explain how HttpClient builds the Observable, what the observe option changes about emissions, and why the stream completes.

for a senior

Spot duplicate requests from repeated subscriptions, use unsubscription to cancel, and decide where a response should be shared.

for a principal

Set conventions for where HTTP Observables are subscribed and shared, so screens do not multiply requests as they grow.

## The symptom Two classic support questions come from the same fact. A developer writes `this.http.post('/api/cart', item);` in a click handler and nothing reaches the server. Another binds `product$ | async` twice in a product-detail template and sees two identical `GET /api/products/42` requests in the network panel. Both follow from HttpClient returning **cold** Observables. This answer assumes Angular 22.2 and RxJS 7.8. ## How HttpClient builds the Observable Inside `HttpClient.request()`, which every shortcut method (`get`, `post`, `put`, …) calls, Angular: 1. Creates an `HttpRequest` object from the URL and options. 2. Builds `of(request).pipe(concatMap((req) => handler.handle(req)))`, where the handler is the interceptor chain ending in the backend. 3. For the default `observe: 'body'`, filters the event stream down to the `HttpResponse` and maps it to its body. Nothing in these steps touches the network. The source comment spells out the intent: running the handler inside the Observable chain means interceptors are **re-run on every subscription**, and retries re-run them too. ## What subscribing does | Action | Result | | --- | --- | | Call `http.get(url)` and drop the result | No request | | Subscribe once | One request; one body emitted; completes | | Subscribe twice (two `async` pipes, or two `subscribe()` calls) | Two independent requests | | Unsubscribe before the response | Request aborted | | `retry(2)` on a failing call | Up to three requests, each through the interceptors | ## Emissions and completion - With `observe: 'body'` (the default) the Observable emits **one** value, the parsed body, then **completes**. - With `observe: 'response'` it emits one `HttpResponse`, including status and headers, then completes. - With `observe: 'events'` it emits several `HttpEvent` values — a sent event, header and progress events when `reportProgress` is set, and the final response — then completes. - A non-2xx status or network failure arrives as an **error** notification carrying an `HttpErrorResponse`; no completion follows. Because the Observable always terminates, a subscription to it cannot stay open indefinitely. Unsubscribing early is still useful, because it **cancels**: in Angular 22 the default backend is `FetchBackend`, whose teardown calls `abort()` on its `AbortController`; with `provideHttpClient(withXhr())` the XHR backend calls `xhr.abort()`. ## Fixing the two symptoms ```ts // Nothing sent: the Observable was never subscribed. save(item: CartItem): void { this.http.post('/api/cart', item); } // Sent: subscribing runs the request. save(item: CartItem): void { this.http.post('/api/cart', item).subscribe(); } ``` For the duplicate `GET`, subscribe once. Options include one `@if (product$ | async; as product)` block that the rest of the template reads from, converting it once with `toSignal()`, or sharing it in a service. Each of those tools has its own trade-offs, covered in their own topics. ## Why cold is the right default - A request is a side effect; making it explicit on subscription means building an Observable never has hidden cost. - Each subscriber gets its own fresh response, so an Observable stored in a field can be re-subscribed later to refetch. - Retrying is just re-subscribing, and interceptors see every attempt — useful for auth headers and logging. ## Things to remember - Calling the method is not sending the request. - One subscription, one request. - Completion is automatic; cancellation is by unsubscribing.

  • Do you need to unsubscribe from an HttpClient Observable to avoid a leak?
    Not for leaks: the Observable completes after the response or errors, which ends the subscription. You unsubscribe when you want to cancel an in-flight request, for example when the user leaves the page, and unsubscribing then aborts it.
  • Why does a retry re-run your auth interceptor?
    Retrying re-subscribes to the source, and HttpClient's Observable runs the whole handler chain, interceptors included, on every subscription. Each attempt therefore goes through the interceptors again, which lets them refresh headers per attempt.

saying these in an interview costs you the question

  • Calling http.get() sends the request immediately.
  • Two async pipes on one HttpClient Observable share one request.
  • An HttpClient Observable stays open until the component is destroyed.
  • Unsubscribing from an HTTP call only ignores the response.
  • HttpClient errors are delivered as a normal next value.
open as a page

In Angular, how do the Observables from HttpClient, Router.events, ActivatedRoute.paramMap and a form control's valueChanges differ in emissions and completion?

level: middleimportance: must knowfreq 58%

basics

~20 s

An HttpClient call is cold: each subscription sends a request, emits one response body and completes. Router.events, ActivatedRoute.paramMap and valueChanges are shared, long-lived streams that never complete; paramMap replays the current params on subscribe, while the other two only emit future events.

open as a page

In Angular reactive forms, what do a control's valueChanges, statusChanges and events Observables emit, and when does each one fire?

level: middleimportance: should knowfreq 44%

basics

~20 s

valueChanges emits the new value and statusChanges the recalculated status each time a control updates, from the UI or code, with no initial value; events emits typed ControlEvent objects for value, status, touched, pristine, submit and reset. None of them completes.

open as a page

In an Angular product-detail page, when does ActivatedRoute.paramMap emit, and why does that matter when navigating from /products/1 to /products/2?

level: middleimportance: should knowfreq 50%

basics

~20 s

paramMap emits the current params synchronously on subscribe and emits again whenever the params change while the component instance is reused; it never completes. Moving from /products/1 to /products/2 reuses the component, so only paramMap, not a one-time snapshot read, sees the new id.

open as a page

In Angular, what does EventEmitter inherit from RxJS Subject, and what goes wrong when a service exposes one as a shared event bus?

level: seniorimportance: should knowfreq 30%

basics

~20 s

EventEmitter extends RxJS Subject and adds emit(): it is hot, has no current value and never completes on its own. Exposed from a service, it lets every consumer emit, error or complete it, which the Subject-based API cannot prevent.

open as a page