Skip to content

Commit 48974c3

Browse files
atscottpkozlowski-opensource
authored andcommitted
fix(core): remove rejectErrors option encourages uncaught exceptions (#60397)
This commit removes the previously added `rejectErrors` option from `toSignal` which was meant to create similar behavior to what happens with unhandled errors in `AsyncPipe`. This option promotes unhandled exceptions and attaches behaviors that we do not believe are desirable as an option offered by the framework: * Unhandled errors that are thrown lose the context of where the error ocurred. Throwing the error where the signal is read allows error handling to be performed at the signal's usage location * With this feature, the value of the signal remains set to the initial value or the previous successful value coming out of the `Observable`. We do not feel this is appropriate implicit behavior but should be an explicit choice by the application. Signals are built to represent state. When an observable stream is converted to a stateful representation, there should be a choice made about what state should be presented when an error occurs * If an error occurs but the signal value is never read in its error state, this is not an application state error that should result in an unhandled exception. * While Angular does not have error boundaries today, there is likely to be investigation in the near future to address increased risk of errors thrown from signals such as this in templates. Without pre-designing any specifics, it is possible that this type of feature would require the error to be thrown from the signal. We are subsequently not prepared to commit to stabilizing the `toSignal` API with the `rejectErrors` option and its current behavior. Developers that desire similar behavior to `rejectErrors` have several options, the simplest of which would be something similar to `toSignal(myStream.pipe(catchError(() => EMPTY)))`. PR Close #60397
1 parent bf5d995 commit 48974c3

File tree

3 files changed

+0
-38
lines changed

3 files changed

+0
-38
lines changed

goldens/public-api/core/rxjs-interop/index.api.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,6 @@ export interface ToSignalOptions<T> {
8484
initialValue?: unknown;
8585
injector?: Injector;
8686
manualCleanup?: boolean;
87-
rejectErrors?: boolean;
8887
requireSync?: boolean;
8988
}
9089

packages/core/rxjs-interop/src/to_signal.ts

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -62,16 +62,6 @@ export interface ToSignalOptions<T> {
6262
*/
6363
manualCleanup?: boolean;
6464

65-
/**
66-
* Whether `toSignal` should throw errors from the Observable error channel back to RxJS, where
67-
* they'll be processed as uncaught exceptions.
68-
*
69-
* In practice, this means that the signal returned by `toSignal` will keep returning the last
70-
* good value forever, as Observables which error produce no further values. This option emulates
71-
* the behavior of the `async` pipe.
72-
*/
73-
rejectErrors?: boolean;
74-
7565
/**
7666
* A comparison function which defines equality for values emitted by the observable.
7767
*
@@ -172,11 +162,6 @@ export function toSignal<T, U = undefined>(
172162
const sub = source.subscribe({
173163
next: (value) => state.set({kind: StateKind.Value, value}),
174164
error: (error) => {
175-
if (options?.rejectErrors) {
176-
// Kick the error back to RxJS. It will be caught and rethrown in a macrotask, which causes
177-
// the error to end up as an uncaught exception.
178-
throw error;
179-
}
180165
state.set({kind: StateKind.Error, error});
181166
},
182167
// Completion of the Observable is meaningless to the signal. Signals don't have a concept of

packages/core/rxjs-interop/test/to_signal_spec.ts

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -152,28 +152,6 @@ describe('toSignal()', () => {
152152
);
153153
});
154154

155-
it('should throw the error back to RxJS if rejectErrors is set', () => {
156-
let capturedObserver: Observer<number> = null!;
157-
const fake$ = {
158-
subscribe(observer: Observer<number>): Unsubscribable {
159-
capturedObserver = observer;
160-
return {unsubscribe(): void {}};
161-
},
162-
} as Subscribable<number>;
163-
164-
const s = toSignal(fake$, {initialValue: 0, rejectErrors: true, manualCleanup: true});
165-
expect(s()).toBe(0);
166-
if (capturedObserver === null) {
167-
return fail('Observer not captured as expected.');
168-
}
169-
170-
capturedObserver.next(1);
171-
expect(s()).toBe(1);
172-
173-
expect(() => capturedObserver.error('test')).toThrow('test');
174-
expect(s()).toBe(1);
175-
});
176-
177155
describe('with no initial value', () => {
178156
it(
179157
'should return `undefined` if read before a value is emitted',

0 commit comments

Comments
 (0)