skip to content

When an Angular resource() loading order history switches users mid-request, what happens to the old response, and why still pass abortSignal to the loader?

level: seniorimportance: should knowfreq 38%

answer

  1. latest request wins
  2. superseded load is discarded
  3. abort stops the actual work
  4. set() and destroy() also abort
  5. reads, not mutations

basics

~10 s

Angular aborts the superseded load and discards its result, so a slow response for the previous user never overwrites the new one. Passing abortSignal to fetch lets that abort cancel the network request itself.

solid answer

~40 s

When `params` changes, the resource moves to `'loading'` for the new user at once, calls `abort()` on the previous load's `AbortController`, and starts a new load. When the old promise eventually settles, Angular checks whether that load's signal was aborted or its request superseded and ignores the result. So stale data cannot land even if your loader ignores the signal. Passing `abortSignal` to `fetch` — or honouring it in your own async code — is what stops the wasted work: the network request is cancelled rather than downloaded and thrown away. The same abort fires when you call `set()` or `update()`, and when the resource is destroyed with its component. Because of this, `resource()` is meant for reads: a param change could abort a mutation half-way.

code

ts · 33 lines
ts
import { Component, resource, signal } from '@angular/core';

interface Order {
  id: string;
  total: number;
}

@Component({
  selector: 'app-support-orders',
  template: `
    <button (click)="select('A')">Customer A</button>
    <button (click)="select('B')">Customer B</button>
    <button (click)="orders.reload()">Refresh</button>
    @if (orders.hasValue()) {
      <p>{{ orders.value().length }} orders</p>
    }
  `,
})
export class SupportOrders {
  readonly selectedId = signal<string | undefined>(undefined);

  readonly orders = resource({
    params: () => this.selectedId(),
    // Passing abortSignal lets Angular cancel the request when A is superseded by B.
    loader: ({ params: id, abortSignal }) =>
      fetch(`/api/customers/${id}/orders`, { signal: abortSignal })
        .then((res) => res.json() as Promise<Order[]>),
  });

  select(id: string): void {
    this.selectedId.set(id);
  }
}

go deeper

for a junior

Recall that a resource keeps only the latest request's result and that the loader receives an abortSignal to hand to fetch.

for a middle

Explain which events abort a load: params change, set() or update(), and destruction, and why reload() does not restart a running load.

for a senior

Separate correctness from cost: Angular discards stale results either way, abortSignal saves the network and server work. Keep writes out of loaders.

for a principal

Set the team rule for where reads and writes live, and how optimistic updates, reloads and server rendering interact with pending tasks.

## The scenario A support dashboard lists customers on the left and shows the selected customer's **order history** on the right, loaded by a `resource()` whose `params` is the selected id. An agent clicks customer A, whose history is large and slow, then immediately clicks customer B. B's response comes back first; A's arrives two seconds later. Which one ends up in `value()`? This answer assumes Angular 22.2. ## What Angular does, step by step 1. **B is selected.** `params` produces a new value. The resource's internal state is a `linkedSignal`, so `status()` flips to `'loading'` **synchronously** and `value()` drops to `undefined` (or the `defaultValue`). 2. **The previous load is aborted.** The internal load effect calls `abort()` on the `AbortController` it created for A's load and releases that load's pending task. 3. **A new load starts** with a fresh `AbortController`; the loader is called with B's id and the new `abortSignal`. 4. **B resolves** and its orders become `value()`, status `'resolved'`. 5. **A settles late.** Before writing anything, Angular checks whether A's signal is aborted, or whether the current request is no longer A's. Both are true, so the result — success or failure — is **discarded**. The outcome is *latest request wins*, built into the resource. The order in which responses arrive does not matter, and your loader does not have to compare ids itself. ## So why pass `abortSignal` at all? Discarding a result and cancelling the work are different things: | Loader ignores `abortSignal` | Loader passes it to `fetch` | | --- | --- | | A's request keeps running to completion | The browser cancels A's request | | The full payload is downloaded, then thrown away | No further bytes are transferred for A | | The server finishes the work for nobody | The server may see the closed connection | | Stale data still never reaches `value()` | Stale data still never reaches `value()` | Correctness is the same either way; **cost** is not. On a list where agents click quickly, ignoring the signal means a queue of abandoned large requests competing with the one that matters. If the loader wraps something other than `fetch`, honour the signal yourself, and expect `fetch` to reject with an abort error — Angular ignores that rejection for a superseded load, so it does not surface in `error()`. ## Every path that aborts a load - **A params change**, as above. - **`set()` or `update()`**: writing a local value moves the resource to `'local'` and aborts any load or reload in progress, so a late response cannot overwrite what you wrote. - **`destroy()`**, or destruction of the injector the resource was created in — typically the component leaving the page. The resource aborts and goes to `'idle'`. `reload()` is the opposite case: while the resource is `'loading'` or `'reloading'` it does **not** restart the request, and it returns `false`. A second click on a refresh button therefore does not cancel and re-send. ## Reads, not mutations The `resource()` API documentation says it is intended for **read** operations, because a new request or a destroy aborts in-progress loads and "could prematurely abort mutations". Cancelling an order, saving a note or posting a refund do not belong in a loader: a user switching customers mid-save would abort the write. Put writes in a service method or event handler, then call `reload()` or `set()` on the resource to reflect the result. ## Pending tasks and server rendering Each load registers a task with Angular's `PendingTasks` and releases it when the load settles or is aborted. That is how server-side rendering knows to wait for resource data before serialising the page, and why a loader that never settles keeps the application from becoming stable. ## Takeaways - Stale responses are dropped by the resource itself; that is correctness. - `abortSignal` makes the abort real on the network; that is efficiency. - Keep mutations out of loaders.

  • Does an aborted fetch rejection show up in error() for the new request?
    No. When the superseded load rejects, Angular sees that its signal was aborted and that the request is no longer current, and returns without touching state. `error()` only reflects failures of the load that is still current.
  • What happens if you call reload() twice quickly on a resource?
    The first call moves it to `'reloading'` and starts the loader. The second call finds the resource already loading, returns `false` and does nothing, so the in-flight request is neither aborted nor duplicated.
  • How would you show an optimistic change after an agent cancels an order?
    Perform the cancellation through a service call outside the resource, then `update()` the resource value to mark the order cancelled. That moves it to `'local'` and aborts any in-flight load that could overwrite it. Call `reload()` afterwards if you want the server's version.

saying these in an interview costs you the question

  • The last response to arrive always wins, so slow requests overwrite fast ones.
  • Ignoring abortSignal lets stale data leak into value().
  • Passing abortSignal is only about correctness, not wasted network work.
  • reload() cancels the in-flight request and starts a fresh one.
  • A resource loader is a good place to post a cancellation or save.