Skip to content

toSignal breaks Change Detection on Errors #51949

@pcbowers

Description

@pcbowers

Which @angular/* package(s) are the source of the bug?

Don't known / other

Is this a regression?

No

Description

When you have an observable that uses the catchError operator to update state when it errors, and when you convert said observable to a signal, then change detection fails after an error is thrown (i.e. the state that is updated by the catchError operator is not visible even after it is changed). If you use the async pipe on the observable ({{ observableData | async }}) instead of using the signal, it works without any additional error handling. After converting to a signal, you are forced to pipe to an additional catchError that converts the error into a value. I've provided a minimum example below that illustrates the problem:

import { NgModule, Component, Input } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { toSignal } from '@angular/core/rxjs-interop';
import { Observable, catchError, throwError, of } from 'rxjs';

@Component({
  selector: 'app-root',
  template: `<div>{{ property }} | {{ signalData() }}</div>
    <app-child [property]="property"></app-child>`,
})
export class AppComponent {
  property = 'No Errors';

  observableData = new Observable<string>((observer) => {
    observer.next('value 1');
    setTimeout(() => observer.next('value 2'), 500);
    setTimeout(() => observer.next('value 3'), 1000);
    setTimeout(() => observer.error(new Error('error')), 1500);
  }).pipe(
    catchError((error) => {
      console.info('setting property...');
      this.property = 'Error Caught';
      return throwError(() => error);
    }),
  );

  signalData = toSignal(this.observableData);

  // Uncomment the below and comment the above to see a working solution
  // Ideally, this wouldn't be necessary

  // signalData = toSignal(
  //   this.observableData.pipe(catchError(() => of('error'))),
  // );
}

@Component({
  selector: 'app-child',
  template: `<div>{{ property }}</div>`,
})
export class ChildComponent {
  @Input()
  property!: string;
}

@NgModule({
  declarations: [AppComponent, ChildComponent],
  imports: [BrowserModule],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

See the attached link to a code sandbox to view the bug in your browser.

A little bit more info: On errors, we dynamically create a component that shows the error message. While the component was created and visible in the dom with the correct template structure, all the component properties embedded in the template only showed their default values, even though we could see the data in the constructor when debugging. After looking around, we noticed that the OnInit lifecycle hook never ran, which we relied on to set our data. When we moved the data into the constructor and forced a change detection cycle manually, it started showing the data in the template, as desired. Triggering a change detection cycle manually did not trigger the OnInit lifecycle hook, but it did make our properties visible in the component.

My guess? When a signal throws an error, any changes that haven't been processed yet before the signal errored aren't ever processed. This fits with what we were seeing in our own project. While it is simple enough to handle errors, it would be nice if it didn't break change detection if we forget to handle them.

Please provide a link to a minimal reproduction of the bug

https://codesandbox.io/p/sandbox/angular-16-change-detection-signals-bug-3tcgcc?file=%2Fsrc%2Fapp%2Fapp.module.ts%3A1%2C1

Please provide the exception or error you saw

No error/exception

Please provide the environment you discovered this bug in (run ng version)

Angular CLI: 16.2.4
Node: 18.15.0
Package Manager: npm 9.8.1
OS: linux x64

Angular: 16.2.7
... animations, common, compiler, compiler-cli, core, forms
... platform-browser, platform-browser-dynamic, router

Package                         Version
---------------------------------------------------------
@angular-devkit/architect       0.1602.4
@angular-devkit/build-angular   16.2.4
@angular-devkit/core            16.2.4
@angular-devkit/schematics      16.2.4
@angular/cdk                    16.2.6
@angular/cli                    16.2.4
@angular/material               16.2.6
@schematics/angular             16.2.4
rxjs                            7.8.1
typescript                      5.1.6
zone.js                         0.13.3

Anything else?

Using signalData without the additional catchError
Using signalData with the additional catchError

Metadata

Metadata

Assignees

Labels

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

Type

No type

Projects

Status

Done

Relationships

None yet

Development

No branches or pull requests

Issue actions