Skip to content

resource.reload() while params is in error switches the resource to 'idle' instead of retrying #71585

Open

Description

@MeAkib

Which @angular/* package(s) are the source of the bug?

core

Is this a regression?

No

Description

When a resource's params function throws (for example a thrown validation error, or mirroring an
upstream resource's error), the resource correctly reports status() === 'error'. Calling reload()
in that state breaks the resource:

  • reload() returns true, but the loader never runs,
  • status() switches from 'error' to 'idle',
  • error() still returns the old error while the status claims 'idle',
  • every subsequent reload() returns false (the 'idle' guard), so a "Retry" button is
    permanently dead.

The cause is in ResourceImpl.reload() (packages/core/src/resource/resource.ts:407):

this.extRequest.update(({request, reload}) => ({request, reload: reload + 1}));

extRequest is a union: after a params error its value is {error, reload}, with no request
field. Destructuring request yields undefined and the rebuilt object drops error, so the state
computation maps request === undefined to 'idle' (resource.ts:320) while reusing the previous —
error — stream (resource.ts:321), which is why error() keeps reporting the stale error.

Expected behavior: reload() should return false and leave the error state untouched when the
error came from params (only a params change can recover it), or alternatively preserve the error
state instead of fabricating 'idle'. Returning true without loading, and reporting 'idle'
while error() is set, are both incoherent.

Minimal reproduction (TestBed spec)

import {ApplicationRef, Injector, resource, signal} from '@angular/core';
import {TestBed} from '@angular/core/testing';

it('reload() while params is in error', async () => {
  const s = signal<string | Error>(new Error('params failed'));
  let loads = 0;
  const res = resource({
    params: () => {
      const v = s();
      if (v instanceof Error) throw v;
      return v;
    },
    loader: async ({params}) => {
      loads++;
      return params;
    },
    injector: TestBed.inject(Injector),
  });
  await TestBed.inject(ApplicationRef).whenStable();

  expect(res.status()).toBe('error'); // ✓

  const result = res.reload();
  await TestBed.inject(ApplicationRef).whenStable();

  console.log(result);        // true  — claims a reload happened
  console.log(res.status());  // 'idle' — was 'error'
  console.log(res.error());   // Error: params failed — still set while 'idle'
  console.log(loads);         // 0     — loader never ran
  console.log(res.reload());  // false — retry permanently dead
});

Observed on main (c27695f278, 22.3.0-next.2); the relevant code is unchanged in 22.x stable.

Please provide a link to a minimal reproduction of the bug

No response

Please provide the exception or error you saw


Please provide the environment you discovered this bug in (run ng version)


Anything else?

No response

Activity

  1. added this to the needsTriage milestone on Oct 24, 2026
  2. added this to the needsTriage milestone on Oct 24, 2026
  3. JeanMeche commented on Oct 25, 2026

    @JeanMeche
    Member

    I would say that your params shouldn't be throwing in the first place. Did you encounter this issue in real world app or did your agent come up with this problem ?

  4. MeAkib commented on Oct 26, 2026

    @MeAkib
    ContributorAuthor

    I would say that your params shouldn't be throwing in the first place. Did you encounter this issue in real world app or did your agent come up with this problem ?

    Fair question. I found it while reading the resource code, not in a production app. But it reproduces with the documented chain() pattern, where params throws by design (ResourceDependencyError). User code doesn't throw anything:

    const userResource = resource({
      params: () => ({id: 1}),
      loader: () => fetchUser(), // fails once, e.g. a 503
    });
    
    const companyResource = resource({
      params: ({chain}) => chain(userResource)?.companyId,
      loader: ({params}) => fetchCompany(params),
    });

    A "Retry" button on the company card calls companyResource.reload():

    Step userResource companyResource companyResource.error() Company loader ran reload() returned
    Initial error error Dependency error 0
    Retry 1 error idle Dependency error 0 true
    Retry 2 error idle Dependency error 0 false
    userResource.reload() succeeds resolved resolved none 1

    So the UI switches from the error state to an empty idle state even though nothing loaded.

    One correction to my description: it isn't permanently stuck. It recovers once userResource reloads successfully, because the params change. The bug is that reload() reports success without loading, and leaves status() and error() disagreeing.

    I'd expect reload() to return false and keep the error state when the error comes from params. I'm happy to send a PR if that's the behaviour you'd want.

  5. wartab commented on Oct 27, 2026

    @wartab
    Contributor

    I think that if a resource sets itself into error-state after an initial crash in params, the resource should not have undefined behaviour in this case either.

    While not entirely the same, we had ran into a similar issue in rxResource where there was a crash in our stream function before returning an observable, which lead to this commit: 9ae9875

    (Which makes me wonder if calling reload() on rxResource in my case above also causes inconsistent behaviours)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: coreIssues related to the framework runtime

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions