Repository navigation
Description
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()returnstrue, 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()returnsfalse(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
JeanMeche commentedon Oct 25, 2026
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 ?
MeAkib commentedon Oct 26, 2026
Fair question. I found it while reading the resource code, not in a production app. But it reproduces with the documented
chain()pattern, whereparamsthrows by design (ResourceDependencyError). User code doesn't throw anything:A "Retry" button on the company card calls
companyResource.reload():userResourcecompanyResourcecompanyResource.error()reload()returnederrorerrorerroridletrueerroridlefalseuserResource.reload()succeedsresolvedresolvedSo the UI switches from the error state to an empty
idlestate even though nothing loaded.One correction to my description: it isn't permanently stuck. It recovers once
userResourcereloads successfully, because the params change. The bug is thatreload()reports success without loading, and leavesstatus()anderror()disagreeing.I'd expect
reload()to returnfalseand keep theerrorstate when the error comes fromparams. I'm happy to send a PR if that's the behaviour you'd want.wartab commentedon Oct 27, 2026
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
rxResourcewhere there was a crash in ourstreamfunction before returning an observable, which lead to this commit: 9ae9875(Which makes me wonder if calling reload() on
rxResourcein my case above also causes inconsistent behaviours)