skip to content

Requests & Typed Responses

HttpClient methods return cold Observables that send nothing until subscribed and cancel on unsubscribe, with observe and responseType shaping the result. Interviewers probe errors and typing.

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

explore

questions

5

With Angular's HttpClient, why does a WeatherService method returning http.get<Forecast>() send no request, and when does it send two?

level: juniorimportance: must knowfreq 74%

answer

  1. a blueprint rather than a request
  2. cold versus hot
  3. count the subscribers
  4. teardown calls abort

basics

~20 s

HttpClient methods return cold Observables: building one sends nothing, every subscription sends its own request, and unsubscribing before the response arrives aborts it. A method that only returns the Observable stays silent until something subscribes.

solid answer

~40 s

`http.get<Forecast>()` does not perform a request; it returns a cold `Observable` that describes one. The request is dispatched only when something subscribes — an explicit `subscribe()`, the `async` pipe or `toSignal()`. Every subscription is independent, so two subscribers to the same Observable send two identical GETs, and each re-runs the interceptor chain. Unsubscribing before the response arrives tears the request down: the default `FetchBackend` (Angular 22) aborts its `AbortController`, and `HttpXhrBackend` calls `xhr.abort()`. After the response, the Observable normally emits once and completes. So a service method that only returns the Observable is correct, while `this.http.post(...)` with no subscriber is a bug: the POST is never sent.

code

ts · 29 lines
ts
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

export interface Forecast {
  city: string;
  tempC: number;
  summary: string;
}

@Injectable({ providedIn: 'root' })
export class WeatherService {
  private readonly http = inject(HttpClient);

  // Correct: returns the blueprint; each caller's subscription sends one GET.
  getForecast(city: string): Observable<Forecast> {
    return this.http.get<Forecast>('/api/forecast', { params: { city } });
  }

  // Bug: nothing subscribes, so the POST is never sent.
  saveFavouriteBroken(city: string): void {
    this.http.post('/api/favourites', { city });
  }

  // Fix: return the Observable and let the caller subscribe (or subscribe here deliberately).
  saveFavourite(city: string): Observable<void> {
    return this.http.post<void>('/api/favourites', { city });
  }
}

go deeper

for a junior

Say plainly that nothing is sent until something subscribes, and that a service should return the Observable rather than subscribe and discard it. Spot the fire-and-forget post() bug on sight.

for a middle

Explain why two subscribers mean two requests: the handler and interceptors run per subscription. Describe what unsubscribing does in the default FetchBackend and why switchMap therefore cancels real requests.

for a senior

Show the production consequences: duplicate calls from double async pipes, retries that re-run interceptors, and aborted writes the server still applied. Recommend idempotent write design rather than relying on cancellation.

for a principal

Weigh where request sharing and cancellation policy should live for a large app: per call site, in data services, or in a signal-based resource layer, and how that choice affects consistency and debuggability.

## What `http.get()` actually returns Angular's `HttpClient` is the injectable service in `@angular/common/http` that performs HTTP calls. Every request method — `get`, `post`, `put`, `patch`, `delete`, `head`, `options` and the generic `request` — returns an RxJS **`Observable`**. It is a **cold** Observable: it holds the recipe for a request (method, URL, body, headers, params, response type) but starts no network activity when it is created. Angular's own guide describes these Observables as *blueprints* for requests. That is why this service method is fine even though nothing seems to happen inside it: ```ts getForecast(city: string): Observable<Forecast> { return this.http.get<Forecast>('/api/forecast', { params: { city } }); } ``` The caller decides when and how often the request runs. ## Subscription is the send button When something subscribes, `HttpClient` runs the request pipeline. In the v22 source the request is wrapped as `of(req).pipe(concatMap((req) => this.handler.handle(req)))`, so the whole handler chain lives *inside* the subscription: 1. The subscriber attaches (an explicit `subscribe()`, the `async` pipe, `toSignal()`, or an operator such as `switchMap` subscribing on your behalf). 2. The interceptor chain runs for this subscription, then the backend receives the final `HttpRequest`. 3. The backend sends the request — `FetchBackend` by default since Angular 22, `HttpXhrBackend` when the app opts in with `withXhr()`. 4. When the full response arrives, the default `observe: 'body'` stream emits the body **once** and **completes**. A non-2xx status goes to the error channel instead. Nothing in steps 2-4 happens without step 1. The classic bug is a fire-and-forget write: ```ts saveFavourite(city: string): void { this.http.post('/api/favourites', { city }); // never subscribed: no POST is sent } ``` ## One subscription, one request Because the pipeline runs per subscription, the Observable is not a cached result: | What the code does | Requests on the wire | |---|---| | Builds `forecast$` and never subscribes | 0 | | Subscribes once (or one `async` pipe) | 1 | | Two `async` pipes on the same `forecast$` in one template | 2 | | `subscribe()` in the component plus an `async` pipe on the same Observable | 2 | | `retry(2)` after a failure | up to 3, each through the interceptor chain again | In code, the difference is easy to see: ```ts const forecast$ = this.weather.getForecast('Oslo'); forecast$.subscribe((f) => this.render(f)); // GET #1 forecast$.subscribe((f) => this.log(f.tempC)); // GET #2, same URL, separate call ``` This surprises people who expect Promise-like behaviour, where the work starts once and every `then` shares the result. With `HttpClient`, sharing one response between subscribers is an explicit choice — for example with RxJS's multicasting operators — and it belongs to the RxJS and request-deduplication topics rather than to `HttpClient` itself. ## Unsubscribing cancels the request The teardown function of the backend's Observable cancels in-flight work: - **`FetchBackend`** (the Angular 22 default) creates an `AbortController` per request and calls `abort()` on it if the subscription ends before the response is done. - **`HttpXhrBackend`** removes its listeners and calls `xhr.abort()` when the request is not yet `DONE`. - A cancelled request **does not emit an error** to the subscriber that left; the subscriber is simply gone. This is why `switchMap` over `HttpClient` gives real cancellation, and why the `async` pipe aborts a pending request when its view is destroyed. One caveat interviewers like: aborting stops the *browser* from waiting. If the request already reached the server, the server may still apply it. Cancelling a non-idempotent POST does not undo it. ## Completion and cleanup After delivering its response, an `HttpClient` Observable normally completes, so a forgotten subscription usually does not leak for long. Interceptors can change that (for example one that emits a cached value and then a fresh one), and a subscription that outlives its component can still run callbacks against destroyed state. The rules for tearing subscriptions down belong to the RxJS-interop topics; the point here is that completion is *usual*, not guaranteed. ## Mistakes to avoid - Expecting `http.get()` or `http.post()` to fire on its own. - Subscribing in a service and returning nothing, so callers cannot cancel or compose. - Binding the same Observable to two `async` pipes and wondering why the network panel shows two calls. - Treating an unsubscribe as a server-side rollback. - Calling a service method from a template, as in `{{ weather.getForecast(city) | async }}`. Every time the view is checked, the method returns a *new* Observable; the `async` pipe drops its old subscription (aborting that request) and subscribes to the new one, and because each arriving value marks the view for another check, the requests can repeat indefinitely. Create the Observable once, in a field or from a signal, and bind that.

  • If a component unsubscribes from an HttpClient POST while it is in flight, can you assume the server did not apply it?
    No. Unsubscribing makes the backend abort the request: `FetchBackend` aborts its `AbortController`, the XHR backend calls `xhr.abort()`. That stops the browser waiting and closes the connection, but if the request already reached the server, the server may finish it. You simply never see the response. For non-idempotent writes, either let them run to completion or make the server tolerate a retry, for example with an idempotency key.
  • What does adding retry(2) to an HttpClient Observable do on the wire?
    It re-subscribes to the cold Observable after a failure, and every subscription runs the whole pipeline again, so up to two further real requests are sent. Each goes through the interceptor chain again, because `HttpClient` runs the handler inside `concatMap` per subscription. That is why an interceptor that reads a fresh token sees each retry, and why retrying a non-idempotent POST can create duplicates.
  • Does an HttpClient Observable complete, and is completion guaranteed?
    With the default `observe: 'body'`, it emits the body once and completes when the response arrives; a non-2xx status or a network failure ends it with an error instead. With `observe: 'events'` it emits several events and then completes. Completion is the usual behaviour, not a guarantee: interceptors can change the stream, for example by emitting a cached value before the fresh response.

An HttpClient Observable is like a recipe card. Holding the card cooks nothing; each cook who follows it makes a separate dish; and a cook who walks away mid-way turns off the stove, though anything already served stays served.

saying these in an interview costs you the question

  • Calling http.get() sends the request at once; subscribe only reads the result.
  • Two subscribers to one HttpClient Observable share a single request and response.
  • Unsubscribing only ignores the response while the request keeps running in the browser.
  • An aborted HttpClient request is guaranteed never to have reached the server.
  • HttpClient Observables never complete, so each subscription stays open until destroyed.
open as a page

With Angular's HttpClient, how are a 404, a network failure and an unparseable 200 response each reported, and what does status 0 mean?

level: middleimportance: must knowfreq 62%

basics

~20 s

All three reach the error channel as an HttpErrorResponse. A 404 keeps status 404 and the error body in error; status 0 means no HTTP response reached the app (offline, CORS block, timeout); an unparseable 200 keeps status 200 and a parsing message.

open as a page

In Angular's HttpClient, why does calling params.set('units', 'metric') on an HttpParams object leave the request's query string unchanged?

level: juniorimportance: should knowfreq 52%

basics

~20 s

HttpParams and HttpHeaders are immutable: set(), append() and delete() return a new instance and leave the original untouched. Discarding the return value discards the change, so reassign the result, chain the calls, or pass a plain object literal instead.

open as a page

With Angular's HttpClient, how do the observe and responseType options change what http.get() emits, for example when you need a response header?

level: middleimportance: should knowfreq 48%

basics

~20 s

observe picks what each emission is: the body (default), the full HttpResponse with status and headers, or the HttpEvent stream. responseType picks how the body is read: json (default), text, blob or arraybuffer. Both need literal values to select the typed overload.

open as a page

An Angular WeatherService uses http.get<Forecast>() and compiles, yet forecast.issuedAt.getHours() throws in production; why did the type not catch it, and how do you harden it?

level: seniorimportance: should knowfreq 38%

basics

~20 s

The generic in http.get<Forecast>() is an unchecked assertion: HttpClient parses the JSON and passes it on without checking it, and JSON has no Date. Type the wire format honestly, validate it in the service, and map it to domain types there.

open as a page