โ† All guides

Angular httpResource, measured โ€” the eager fetch, the URL that never refetches, and the value() that throws

httpResource is Angular's signal-native way to fetch: declare a URL as a function of signals and the framework refetches, cancels, and tracks loading state for you. It genuinely removes whole bug classes โ€” but it has sharp edges the docs don't lead with: it fetches the moment the class is constructed (not when the template needs it), a URL built outside the reactive function silently never refetches, and value() throws when the request failed.

Everything below was reproduced and measured in a real Chrome on Angular 22.1 (stock ng new blueprint) with an instrumented window.fetch before publishing. The numbers โ€” 5 requests from 5 hidden sections, a UI stuck on page 1 while claiming page 3, 81 fetches in 5 seconds from one effect, a stale response clobbering a fresh one โ€” are observed counts, not estimates.

1. Try it in 10 seconds (no signup)

A shared, self-resetting demo project is live right now:

curl "https://mockbird.mockbird.workers.dev/m/demo/products?limit=3"

That URL is the API for every snippet below. CORS is open (Access-Control-Allow-Origin: *), so it works from localhost:4200, StackBlitz, CI, anywhere โ€” no proxy.conf.json. When you want your own schema it's one curl or one click โ€” see ยง10.

โšก Skip the terminal: this link creates a live, seeded e-commerce backend (products, orders, customers, reviews) in the dashboard โ€” real URL, no signup. Or import your own OpenAPI spec, db.json, CSV, Postman collection, or HAR and mock your exact shapes. Setup: httpResource lives in @angular/common/http (experimental from 19.2, stable public API since 22.0 โ€” we checked the @publicApi 22.0 tag in the shipped types; snippets measured on 22.1) and needs provideHttpClient() in your providers.

2. It fetches on construction โ€” @if won't save you

httpResource is eager: the request fires when the class field initializes, not when the template first reads the resource. We declared five resources in one component and rendered exactly one section behind @if:

// five httpResource fields declared, template shows ONE @if branch
res      = httpResource<Product[]>(() => `${API}/products?limit=5`);
raceRes  = httpResource<Product[]>(() => `${API}/products?limit=1`);
errRes   = httpResource<Product[]>(() => `${API}/products?limit=2`);
pageRes  = httpResource<Product[]>(() => `${API}/products?page=1`);
seqRes   = httpResource<Product[]>(() => `${API}/products?limit=1`);

Measured: 5 network requests on page load โ€” every hidden section fetched. The escape hatch is documented but easy to miss: return undefined from the URL function and that resource won't fetch.

res = httpResource<Product[]>(() =>
  this.show() ? `${API}/products?limit=5` : undefined);  // no request until show() is true

Re-measured with the guard: exactly 1 request. If a resource belongs to a tab the user may never open, gate it โ€” or move it into the child component that actually renders it.

3. The happy path: read the signal inside the URL function

import { Component, signal } from '@angular/core';
import { httpResource } from '@angular/common/http';

@Component({ /* ... */ })
export class ProductList {
  page = signal(1);
  res = httpResource<Product[]>(() =>
    `https://mockbird.mockbird.workers.dev/m/demo/products?page=${this.page()}&limit=5`);
}
// template:
// @for (p of res.value() ?? []; track p.id) { <li>{{ p.name }}</li> }
// <button (click)="page.set(page() + 1)">next</button>

Measured: exactly 1 request on load; clicking next produced exactly 1 refetch and the five rows swapped to page 2. No subscription management, no ngOnChanges, no manual refetch call โ€” the URL function read this.page(), so the resource depends on it.

4. The URL that never refetches (zero warnings)

The subtlest failure is building the URL string outside the reactive function โ€” in a field initializer, the constructor, or a helper called once:

// THE BUG: page() is read ONCE, outside the reactive context
const url = `${API}/products?page=${this.page()}&limit=5`;
res = httpResource<Product[]>(() => url);   // closes over a frozen string

Measured: we clicked "next page" twice โ€” the page counter in the template dutifully rendered "page: 3", and the network log showed 1 request, total. The list stayed frozen on page-1 rows. Zero console warnings, zero errors. Nothing tells you the dependency was never tracked; the UI just quietly stops agreeing with itself. The fix is mechanical: interpolate signals inside the function you hand to httpResource, never before it.

5. Cancellation is built in โ€” the naive version renders stale data

What happens when params change while a request is still in flight? We made the first request slow (?mock_delay=2500) and switched params 400ms in:

q = signal('slow');
res = httpResource<Product[]>(() => {
  const delay = this.q() === 'slow' ? 2500 : 0;
  return `${API}/products?limit=1&mock_delay=${delay}`;
});

Measured: the in-flight slow request was aborted the moment the signal changed (we captured the AbortSignal firing), the fast response rendered โ€” and 2.5 seconds later, when the slow response would have landed, nothing changed. No stale overwrite, for free.

The same race hand-rolled with effect() + fetch โ€” no abort โ€” did what naive fetch code always does: the fast result rendered first, then the stale slow response landed 2.5s later and clobbered it. If you're fetching in effects today, this is the bug you have and haven't noticed. (?mock_delay makes it reproducible on demand instead of "sometimes, on hotel wifi".)

6. The effect() fetch storm: 81 requests in 5 seconds, silently

The classic signal loop โ€” an effect reads a signal that its own fetch callback writes:

items = signal<Product[]>([]);
constructor() {
  effect(() => {
    const current = this.items();                    // READ
    fetch(`${API}/products?limit=3`)
      .then(r => r.json())
      .then(d => this.items.set(d));                 // WRITE โ†’ re-run โ†’ forever
  });
}

Measured: 32 requests by the 2-second mark, 81 by 5 seconds, and it never stops. Every response is a fresh array reference, so the signal always "changed", so the effect always re-runs. Because the write happens in an async .then(), no synchronous-loop detection fires: zero console errors, zero warnings, and the page looks perfectly normal while your API burns. This is precisely the pattern httpResource exists to replace โ€” the ยง3 version is immune by construction, because the response value isn't part of the URL computation.

7. Error and loading states โ€” value() throws, and reload โ‰  param change

Simulate the failure first, then write the handler. ?mock_status=500 makes the demo return a real 500:

curl -s -o /dev/null -w "%{http_code}\n" \
  "https://mockbird.mockbird.workers.dev/m/demo/products?limit=2&mock_status=500"

Measured against a 500: status() โ†’ 'error', error() โ†’ an HttpErrorResponse with .status === 500, hasValue() โ†’ false โ€” and value() throws:

Error: Resource is currently in an error state (see Error.cause for details):
  Http failure response for .../products?mock_status=500: 500 Internal Server Error

A template that reads res.value() without a guard doesn't render an empty list on failure โ€” it throws during render. Guard with hasValue() (it narrows the type) or use @if/@else blocks over status().

Two loading states look identical in a spinner but behave differently under your data. Measured mid-flight:

status()hasValue()value()
After reload()'reloading'trueprevious data, still there
After a param change'loading'falseundefined โ€” your list blanks

So a refresh button keeps rows on screen, but a page change flashes empty unless you handle it. Make both paths visible with ?mock_delay=2000 and watch what your UI actually does during the gap.

8. Pagination with X-Total-Count โ€” headers are a signal too

hPage = signal(1);
pageRes = httpResource<Product[]>(() =>
  `${API}/products?page=${this.hPage()}&limit=10`);
total = computed(() =>
  Number(this.pageRes.headers()?.get('X-Total-Count') ?? 0));

Measured: headers() exposed X-Total-Count: 30 (it's CORS-exposed on the demo โ€” many real APIs forget Access-Control-Expose-Headers and this silently reads null). A load-more accumulator went 10 โ†’ 20 โ†’ 30 of 30 in exactly 3 requests with 0 duplicate ids, and the button's [disabled] binding flipped at the boundary.

9. Deterministic retry tests โ€” reload() against a scripted outage

Retry logic tested against a healthy API is untested. ?mock_seq serves an exact status script:

curl -s -o /dev/null -w "%{http_code}\n" \
  "https://mockbird.mockbird.workers.dev/m/demo/products?limit=1&mock_seq=503,503,200&mock_seq_key=me1"

Measured: an httpResource pointed at that URL landed in status() === 'error' with a 503; reload() โ†’ 503 again; second reload() โ†’ 'resolved' with data. Exactly 3 requests. reload() is the retry primitive โ€” wire it to an "our fault, try again" button or drive it from an effect with backoff. On the shared demo the sequence counter is scoped per client IP (and mock_seq_key isolates parallel specs), so the curl above starts fresh for you. Statuses โ‰ฅ400 are simulated before processing, so a failed write is never half-applied.

10. Get your own API (10 seconds, no signup)

curl -X POST https://mockbird.mockbird.workers.dev/api/projects \
  -H "Content-Type: application/json" \
  -d '{"preset": "ecommerce"}'

The response contains your project URL and admin key. Presets: blog, ecommerce, saas, payments โ€” or define your own resources and field types, import an OpenAPI spec / db.json / CSV / Postman collection / HAR, and swap the base URL in the snippets above. Ship day is one environment token change.

11. Beyond this guide

12. Honest comparison

ToolGood atWhere this differs
angular-in-memory-web-apiThe classic Angular mock backend: intercepts HttpClient in-process, zero networkLives inside your build โ€” curl can't hit it, teammates and CI E2E can't share it, and it never exercises real CORS/abort/header behavior (ยง5 and ยง8 are invisible to it). Maintenance has also lagged major Angular releases. Mockbird is a real URL over a real network.
MSW (Mock Service Worker)In-process request interception for unit tests; no network at allMSW mocks live inside each test runner. A hosted mock is the same URL for your browser, CI, a StackBlitz repro, and a teammate's machine with zero setup. Use MSW for unit tests, a hosted mock for everything shared.
HttpTestingControllerAngular's own request-assertion harness for TestBed unit testsIt verifies that your code requested โ€” it never serves data to a running app in a browser. Complementary, not competing: unit-test with it, point the running app here.
json-serverLocal full-featured fake REST; huge ecosystemNeeds Node running on every machine that wants the API. Mockbird speaks the same conventions (_page, _limit, db.json import/export) but is hosted โ€” and adds auth simulation, failure injection (mock_seq, mock_status, mock_delay), snapshots.

Bias disclosure: this comparison is written by the Mockbird side. The measurements above, though, are just measurements โ€” rerun them in your own Chrome with the snippets as written.