-
Notifications
You must be signed in to change notification settings - Fork 27.1k
Description
Which @angular/* package(s) are the source of the bug?
core
Is this a regression?
No
Description
Before I begin I want to stress this issue is regarding confusing error reporting and not the signals implementation or design itself. The situation described below is most likely to be encountered when migrating from Observables to Signals, and can lead to confusion.
Sample Code
Here is a simple enough computed.
It uses toSignal inside the computed, but let's assume the user is new to Signals and doesn't know they shouldn't do that..
// guaranteed synchronous observables
const store = {
first$: of('Simon'),
last$: of('Weaver')
};
// simple computed (inside injection context)
const fullName = computed(() =>
{
const first = toSignal(store.first$, { requireSync: true });
const last = toSignal(store.last$, { requireSync: true });
return first() + ' ' + last();
});
console.log('full name ', fullName());
Output
The above code reports two errors, the first of which is incorrect and misleading.
Error: NG0601:
toSignal()called withrequireSyncbutObservabledid not emit synchronously.
Error: NG0600: Writing to signals is not allowed in acomputedor aneffectby default. UseallowSignalWritesin theCreateEffectOptionsto enable this inside effects.
It turns out the first error is a side effect of the second error (despite being printed out first). If you only notice the first error (and personally I like to put breakpoints inside the vendor code where they are raised!) then you will be misled.
Explanation
Angular error NG0601 is triggered when an Observable does not emit synchronously when calling toSignal.
NG0601 `toSignal()` called with `requireSync` but `Observable` did not emit synchronously.
This works internally by subscribing to the observable and checking immediately to see if the corresponding signal has been assigned a value.
angular/packages/core/rxjs-interop/src/to_signal.ts
Lines 179 to 184 in 25153e9
| const sub = source.subscribe({ | |
| next: value => state.set({kind: StateKind.Value, value}), | |
| error: error => state.set({kind: StateKind.Error, error}), | |
| // Completion of the Observable is meaningless to the signal. Signals don't have a concept of | |
| // "complete". | |
| }); |
This sets a signal (which begins as StateKind.NoValue) to the synchronous value of the observable, which can then be checked if requireSync == true.
However since ALL writes to signals in computed and effect are forbidden this signal cannot be set, therefore it remains in StateKind.NoValue state, thus triggering the erroneous message:
NG0601 `toSignal()` called with `requireSync` but `Observable` did not emit synchronously.
Possible Solution
This could easily be solved by introducing a boolean to track the synchronous emission instead of relying on the state of the internal toSignal signal:
const syncValue = false; // NEW
const sub = source.subscribe({
next: value => { syncValue = true; state.set({kind: StateKind.Value, value}); },
error: error => { syncValue = true; state.set({kind: StateKind.Error, error}); },
// Completion of the Observable is meaningless to the signal. Signals don't have a concept of
// "complete".
});
if (ngDevMode && options?.requireSync && !syncValue) {
throw new RuntimeError(
RuntimeErrorCode.REQUIRE_SYNC_WITHOUT_SYNC_EMIT,
'`toSignal()` called with `requireSync` but `Observable` did not emit synchronously.');
}