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 with requireSync but Observable did not emit synchronously.
Error: NG0600: Writing to signals is not allowed in a computed or an effect by default. Use allowSignalWrites in the CreateEffectOptions to 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.
|
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.');
}
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
toSignalinside thecomputed, but let's assume the user is new to Signals and doesn't know they shouldn't do that..Output
The above code reports two errors, the first of which is incorrect and misleading.
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.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
This sets a signal (which begins as
StateKind.NoValue) to the synchronous value of the observable, which can then be checked ifrequireSync == true.However since ALL writes to signals in
computedandeffectare forbidden this signal cannot be set, therefore it remains inStateKind.NoValuestate, thus triggering the erroneous message: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
toSignalsignal: