-
Notifications
You must be signed in to change notification settings - Fork 26.5k
Description
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
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?
Metadata
Metadata
Assignees
Labels
Type
Projects
Status