Repository navigation
Description
Which @angular/* package(s) are the source of the bug?
forms
Is this a regression?
No
Description
Let's say we have a function like this:
type Result = { loading: true } | { loading: false; value: string }
function injectResult(value: () => string): Signal<Result> {
// The implementation could be more complex than this,
// but the issue still happens with only this code.
return computed(() => ({ loading: false, value: value() }))
}
const value = signal('Hello Angular!')
const result: Signal<Result> = injectResult(() => value())We can convert it to a resource like this:
function toSnapshot(result: Result) {
if (result.loading) {
return { status: 'loading', value: undefined }
}
return { status: 'resolved', value: result.value }
}
function toResource(result: Signal<Result>) {
return resourceFromSnapshots(() => toSnapshot(result()))
}Since validateAsync works with the Resource interface (not the API), one might expect this to work:
@Component({ ... })
export class Registration {
registrationModel = signal({ username: '' })
registrationForm = form(this.registrationModel, (schemaPath) => {
validateAsync(schemaPath.username, {
params: ({ value }) => value(),
factory: (value) => {
const result = injectResult(value)
return toResource(result)
},
onSuccess: () => {},
onError: () => {},
})
})
}This works with those options, but if we add debounce to the validateAsync options, we get:
ERROR _ResourceParamsStatus: LOADING
The workaround is to wrap the validation signal in a resource to catch the internal sentinel error:
factory: (value) => {
const settled = resource({
params: () => value(),
loader: async ({ params }) => params,
})
const result = injectResult(() => {
return settled.hasValue() ? settled.value() : ''
})
return toResource(result)
}The expectation is that the signal passed to factory contains the debounced value, rather than a signal that may throw.
Requiring that signal to be used with the resource API (not just the interface) makes it harder for libraries that do not use that API to integrate with Signal Forms, but can provide a Resource Interface. For example, libraries like a resouce-like API for localstorage and query/sync libraries.
Please provide a link to a minimal reproduction of the bug
https://github.com/benjavicente/angular-validate-async-resource-repro
Please provide the exception or error you saw
Please provide the environment you discovered this bug in (run ng version)
┌───────────────────────────┬───────────────────┬───────────────────┐
│ Package │ Installed Version │ Requested Version │
├───────────────────────────┼───────────────────┼───────────────────┤
│ @angular/build │ 22.1.8 │ ^22.0.0 │
│ @angular/cli │ 22.1.8 │ ^22.0.0 │
│ @angular/common │ 22.1.7 │ ^22.0.0 │
│ @angular/compiler │ 22.1.7 │ ^22.0.0 │
│ @angular/compiler-cli │ 22.1.7 │ ^22.0.0 │
│ @angular/core │ 22.1.7 │ ^22.0.0 │
│ @angular/forms │ 22.1.7 │ ^22.0.0 │
│ @angular/platform-browser │ 22.1.7 │ ^22.0.0 │
│ rxjs │ 7.8.2 │ ~7.8.0 │
│ typescript │ 6.0.3 │ ~6.0.2 │
└───────────────────────────┴───────────────────┴───────────────────┘
Anything else?
No response
Metadata
Metadata
Assignees
Type
Projects
- StatusShow more project fieldsNo status
Activity
fix(forms): expose safe params signal to validateAsync debounce factory
fix(forms): replace ɵchain with snapshot composition in validateAsync…
fix(forms): compose debounced async validation snapshots
fix(forms): compose debounced async validation snapshots
JeanMeche commentedon Sep 29, 2026
The root cause here can be illustrated as following with resource chaining + debounced.
benjavicente commentedon Sep 29, 2026
I would add the case where the chained signal is used in an effect:
JeanMeche commentedon Sep 29, 2026
I did miss it earlier, but our docs specifically mentions that
chaincan and will throw when the resource is loading and the consequence here is that reading the factoryvalueis also unsafe in case of a debounce.There would be different ways to tackle this:
benjavicente commentedon Sep 30, 2026
I think the issue is that
chainis assumed to be called only in the params function, sovaldiateAsyncbreaks that promise by chaining a resource outside of it to be passed as a signal to the validation factory. It seems like it's the only API that calls the chain API that way.So not something related to chain itself, but the implementation/API shape of
validateAsyncspecifically.In normal uses cases of chain, it is controlled when you create the resource itself, so the user has full control of it:
But in validate
valdiateAsync, the chain is hidden and not part of any params function:My impressions is that if
valdiateAsyncwanted to support resource chaining it should have passed a resource into the factory. Since the API does not, it worked around that by doing an implicitchain, witch works if the user uses only the resource API and breaks with any other API like effects. To fix that it seems to need a utility to compose the resources in a safe way since it cain chain implicitly without leaking the error from chain (since, it's only the params function from the resource api that should catch it, not a factory or resoruce).This is essentially what PRs that have linked to this issue did
fix(forms): safely chain debounced async validation resources
benjavicente commentedon Oct 26, 2026
Bringin here the comments of #71414 (comment) and #71613 (comment) here for visibility
The usage of
ɵchaininside the implementation ofvalidateAsyncshould be removed. Why: In the docschain, is available in theparamscontext object of the resource API. By calling it internally in signal forms, it forces consumers to use the Resource API:Factories that return the Resource Interface but do not use the Resource API don't work with
validateAsyncbecause they don't have a way of catching the implicit chain. No other utility in Angular callsɵchainoutside of the params function, all other examples ofchaincall that function inside the givenparamsfunction.validateAsyncis the only place it doesn't.So how
paramsare given to thefactoryshould change if that API should maintain its interface as is.Option 1: Don't throw and retain the previous value: Doesn't work because it introduces behavioural issues, like on
A->B->Achains.Option 2: Don't throw and return undefined: Doesn't work because it will make resource start in an idle state, breaking current tests, because it makes the resulting resource
idleinstead ofloading.Option 3: Throw, and add something to catch it: There could be mechanisms to catch the pending state. Automatically handling these markers across reactive consumers would be a broader architectural change (like Solid v2), something Angular does not do at the moment. So instead there could be an explicit function, let's say
wrapChainedthat takes a chained signal and returns a Resource, where the usage of that would only be required in validateAsync for non resource API usage. Both options don't seem great from an API design standpoint.For option 1 and 2, there is also other issues with how chaining works, like the forwarding issues.
Those are the only 3 options that look available without changing the API shape. So the preferable path forward might be to create a new API that does not use
chaininternally, allowing resources to be composed by the user of the validation utility.PR #71613 presents the idea of a API with the following shape:
Chaining is explicit, and
validateResourcedoes an additional "safe-chain" internally to ensure the correct resource state is used.