Skip to content

Commit f9120d7

Browse files
crisbetoatscott
authored andcommitted
fix(core): allow effect to be used inside an ErrorHandler (#53713)
`effect` was expecting an `ErrorHandler` in its constructor which can lead to a circular DI error if an effect is used inside a custom `ErrorHandler`. These changes inject the `ErrorHandler` only when reporting errors. Fixes #52680. PR Close #53713
1 parent 0b8a649 commit f9120d7

File tree

2 files changed

+47
-5
lines changed

2 files changed

+47
-5
lines changed

packages/core/src/render3/reactivity/effect.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,8 @@ class EffectHandle implements EffectRef, SchedulableEffect {
182182
constructor(
183183
private scheduler: EffectScheduler,
184184
private effectFn: (onCleanup: EffectCleanupRegisterFn) => void,
185-
public creationZone: Zone|null, destroyRef: DestroyRef|null,
186-
private errorHandler: ErrorHandler|null, allowSignalWrites: boolean) {
185+
public creationZone: Zone|null, destroyRef: DestroyRef|null, private injector: Injector,
186+
allowSignalWrites: boolean) {
187187
this.watcher = createWatch(
188188
(onCleanup) => this.runEffect(onCleanup), () => this.schedule(), allowSignalWrites);
189189
this.unregisterOnDestroy = destroyRef?.onDestroy(() => this.destroy());
@@ -193,7 +193,10 @@ class EffectHandle implements EffectRef, SchedulableEffect {
193193
try {
194194
this.effectFn(onCleanup);
195195
} catch (err) {
196-
this.errorHandler?.handleError(err);
196+
// Inject the `ErrorHandler` here in order to avoid circular DI error
197+
// if the effect is used inside of a custom `ErrorHandler`.
198+
const errorHandler = this.injector.get(ErrorHandler, null, {optional: true});
199+
errorHandler?.handleError(err);
197200
}
198201
}
199202

@@ -273,12 +276,11 @@ export function effect(
273276

274277
!options?.injector && assertInInjectionContext(effect);
275278
const injector = options?.injector ?? inject(Injector);
276-
const errorHandler = injector.get(ErrorHandler, null, {optional: true});
277279
const destroyRef = options?.manualCleanup !== true ? injector.get(DestroyRef) : null;
278280

279281
const handle = new EffectHandle(
280282
injector.get(APP_EFFECT_SCHEDULER), effectFn,
281-
(typeof Zone === 'undefined') ? null : Zone.current, destroyRef, errorHandler,
283+
(typeof Zone === 'undefined') ? null : Zone.current, destroyRef, injector,
282284
options?.allowSignalWrites ?? false);
283285

284286
// Effects need to be marked dirty manually to trigger their initial run. The timing of this

packages/core/test/render3/reactivity_spec.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,46 @@ describe('effects', () => {
6666
expect(lastError.message).toBe('fail!');
6767
});
6868

69+
it('should be usable inside an ErrorHandler', async () => {
70+
const shouldError = signal(false);
71+
let lastError: any = null;
72+
73+
class FakeErrorHandler extends ErrorHandler {
74+
constructor() {
75+
super();
76+
effect(() => {
77+
if (shouldError()) {
78+
throw new Error('fail!');
79+
}
80+
});
81+
}
82+
83+
override handleError(error: any): void {
84+
lastError = error;
85+
}
86+
}
87+
88+
@Component({
89+
standalone: true,
90+
template: '',
91+
providers: [{provide: ErrorHandler, useClass: FakeErrorHandler}]
92+
})
93+
class App {
94+
errorHandler = inject(ErrorHandler);
95+
}
96+
97+
const fixture = TestBed.createComponent(App);
98+
fixture.detectChanges();
99+
100+
expect(fixture.componentInstance.errorHandler).toBeInstanceOf(FakeErrorHandler);
101+
expect(lastError).toBe(null);
102+
103+
shouldError.set(true);
104+
fixture.detectChanges();
105+
106+
expect(lastError?.message).toBe('fail!');
107+
});
108+
69109
it('should run effect cleanup function on destroy', async () => {
70110
let counterLog: number[] = [];
71111
let cleanupCount = 0;

0 commit comments

Comments
 (0)