Skip to content

Commit

Permalink
fix(core): allow effect to be used inside an ErrorHandler (#53713)
Browse files Browse the repository at this point in the history
`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
  • Loading branch information
crisbeto authored and atscott committed Jan 3, 2024
1 parent 0b8a649 commit f9120d7
Show file tree
Hide file tree
Showing 2 changed files with 47 additions and 5 deletions.
12 changes: 7 additions & 5 deletions packages/core/src/render3/reactivity/effect.ts
Expand Up @@ -182,8 +182,8 @@ class EffectHandle implements EffectRef, SchedulableEffect {
constructor(
private scheduler: EffectScheduler,
private effectFn: (onCleanup: EffectCleanupRegisterFn) => void,
public creationZone: Zone|null, destroyRef: DestroyRef|null,
private errorHandler: ErrorHandler|null, allowSignalWrites: boolean) {
public creationZone: Zone|null, destroyRef: DestroyRef|null, private injector: Injector,
allowSignalWrites: boolean) {
this.watcher = createWatch(
(onCleanup) => this.runEffect(onCleanup), () => this.schedule(), allowSignalWrites);
this.unregisterOnDestroy = destroyRef?.onDestroy(() => this.destroy());
Expand All @@ -193,7 +193,10 @@ class EffectHandle implements EffectRef, SchedulableEffect {
try {
this.effectFn(onCleanup);
} catch (err) {
this.errorHandler?.handleError(err);
// Inject the `ErrorHandler` here in order to avoid circular DI error
// if the effect is used inside of a custom `ErrorHandler`.
const errorHandler = this.injector.get(ErrorHandler, null, {optional: true});
errorHandler?.handleError(err);
}
}

Expand Down Expand Up @@ -273,12 +276,11 @@ export function effect(

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

const handle = new EffectHandle(
injector.get(APP_EFFECT_SCHEDULER), effectFn,
(typeof Zone === 'undefined') ? null : Zone.current, destroyRef, errorHandler,
(typeof Zone === 'undefined') ? null : Zone.current, destroyRef, injector,
options?.allowSignalWrites ?? false);

// Effects need to be marked dirty manually to trigger their initial run. The timing of this
Expand Down
40 changes: 40 additions & 0 deletions packages/core/test/render3/reactivity_spec.ts
Expand Up @@ -66,6 +66,46 @@ describe('effects', () => {
expect(lastError.message).toBe('fail!');
});

it('should be usable inside an ErrorHandler', async () => {
const shouldError = signal(false);
let lastError: any = null;

class FakeErrorHandler extends ErrorHandler {
constructor() {
super();
effect(() => {
if (shouldError()) {
throw new Error('fail!');
}
});
}

override handleError(error: any): void {
lastError = error;
}
}

@Component({
standalone: true,
template: '',
providers: [{provide: ErrorHandler, useClass: FakeErrorHandler}]
})
class App {
errorHandler = inject(ErrorHandler);
}

const fixture = TestBed.createComponent(App);
fixture.detectChanges();

expect(fixture.componentInstance.errorHandler).toBeInstanceOf(FakeErrorHandler);
expect(lastError).toBe(null);

shouldError.set(true);
fixture.detectChanges();

expect(lastError?.message).toBe('fail!');
});

it('should run effect cleanup function on destroy', async () => {
let counterLog: number[] = [];
let cleanupCount = 0;
Expand Down

0 comments on commit f9120d7

Please sign in to comment.