Skip to content

Error NG0601 is reported incorrectly (triggered by NG0600) when calling toSignal inside a computed or effect  #51027

@simeyla

Description

@simeyla

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.');
  }

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: coreIssues related to the framework runtimecore: reactivityWork related to fine-grained reactivity in the core frameworkcross-cutting: signals

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions