Skip to content

Commit 338818c

Browse files
atscottAndrewKushnir
authored andcommitted
fix(core): Surface errors from ApplicationRef.tick to callsite (#60102)
This commit ensures that errors during `ApplicationRef.tick` are surfaced to the callsite rather than being caught and reported to the `ErrorHandler`. The current catch and report approach was originally added in e263e19 with the goal of preventing automatic change detection crashes due to the error happening in the subscription. However, this results in hiding a public API that can hide errors. Callers cannot assume that the tick was successful and perform follow-up work. This change now surfaces errors and adds the error handling directly to the callsites. BREAKING CHANGE: `ApplicationRef.tick` will no longer catch and report errors to the appplication `ErrorHandler`. Errors will instead be thrown out of the method and will allow callers to determine how to handle these errors, such as aborting follow-up work or reporting the error and continuing. PR Close #60102
1 parent a02e270 commit 338818c

4 files changed

Lines changed: 18 additions & 10 deletions

File tree

packages/core/src/application/application_ref.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ import {Injectable} from '../di/injectable';
2323
import {InjectionToken} from '../di/injection_token';
2424
import {Injector} from '../di/injector';
2525
import {EnvironmentInjector, type R3Injector} from '../di/r3_injector';
26-
import {ErrorHandler, INTERNAL_APPLICATION_ERROR_HANDLER} from '../error_handler';
2726
import {formatRuntimeError, RuntimeError, RuntimeErrorCode} from '../errors';
27+
import {ErrorHandler, INTERNAL_APPLICATION_ERROR_HANDLER} from '../error_handler';
2828
import {Type} from '../interface/type';
2929
import {ComponentFactory, ComponentRef} from '../linker/component_factory';
3030
import {ComponentFactoryResolver} from '../linker/component_factory_resolver';
@@ -603,9 +603,6 @@ export class ApplicationRef {
603603
view.checkNoChanges();
604604
}
605605
}
606-
} catch (e) {
607-
// Attention: Don't rethrow as it could cancel subscriptions to Observables!
608-
this.internalErrorHandler(e);
609606
} finally {
610607
this._runningTick = false;
611608
this.tracingSnapshot?.dispose();
@@ -754,7 +751,11 @@ export class ApplicationRef {
754751

755752
private _loadComponent(componentRef: ComponentRef<any>): void {
756753
this.attachView(componentRef.hostView);
757-
this.tick();
754+
try {
755+
this.tick();
756+
} catch (e) {
757+
this.internalErrorHandler(e);
758+
}
758759
this.components.push(componentRef);
759760
// Get the listeners lazily to prevent DI cycles.
760761
const listeners = this._injector.get(APP_BOOTSTRAP_LISTENER, []);

packages/core/src/change_detection/scheduling/ng_zone_scheduling.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,14 @@ import {
3131
SCHEDULE_IN_ROOT_ZONE,
3232
} from './zoneless_scheduling';
3333
import {SCHEDULE_IN_ROOT_ZONE_DEFAULT} from './flags';
34-
import {ErrorHandler, INTERNAL_APPLICATION_ERROR_HANDLER} from '../../error_handler';
34+
import {INTERNAL_APPLICATION_ERROR_HANDLER, ErrorHandler} from '../../error_handler';
3535

3636
@Injectable({providedIn: 'root'})
3737
export class NgZoneChangeDetectionScheduler {
3838
private readonly zone = inject(NgZone);
3939
private readonly changeDetectionScheduler = inject(ChangeDetectionScheduler);
4040
private readonly applicationRef = inject(ApplicationRef);
41+
private readonly applicationErrorHandler = inject(INTERNAL_APPLICATION_ERROR_HANDLER);
4142

4243
private _onMicrotaskEmptySubscription?: Subscription;
4344

@@ -55,7 +56,11 @@ export class NgZoneChangeDetectionScheduler {
5556
return;
5657
}
5758
this.zone.run(() => {
58-
this.applicationRef.tick();
59+
try {
60+
this.applicationRef.tick();
61+
} catch (e) {
62+
this.applicationErrorHandler(e);
63+
}
5964
});
6065
},
6166
});

packages/core/src/change_detection/scheduling/zoneless_scheduling_impl.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
ZONELESS_SCHEDULER_DISABLED,
3232
} from './zoneless_scheduling';
3333
import {TracingService} from '../../application/tracing';
34+
import {INTERNAL_APPLICATION_ERROR_HANDLER} from '../../error_handler';
3435

3536
const CONSECUTIVE_MICROTASK_NOTIFICATION_LIMIT = 100;
3637
let consecutiveMicrotaskNotifications = 0;
@@ -57,6 +58,7 @@ function trackMicrotaskNotificationForDebugging() {
5758

5859
@Injectable({providedIn: 'root'})
5960
export class ChangeDetectionSchedulerImpl implements ChangeDetectionScheduler {
61+
private readonly applicationErrorHandler = inject(INTERNAL_APPLICATION_ERROR_HANDLER);
6062
private readonly appRef = inject(ApplicationRef);
6163
private readonly taskService = inject(PendingTasksInternal);
6264
private readonly ngZone = inject(NgZone);
@@ -292,7 +294,7 @@ export class ChangeDetectionSchedulerImpl implements ChangeDetectionScheduler {
292294
);
293295
} catch (e: unknown) {
294296
this.taskService.remove(task);
295-
throw e;
297+
this.applicationErrorHandler(e);
296298
} finally {
297299
this.cleanup();
298300
}

packages/core/test/render3/reactivity_spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ describe('reactivity', () => {
9191
expect(isStable).toEqual([true, false, true]);
9292
});
9393

94-
it('should propagate errors to the ErrorHandler', () => {
94+
it('should propagate errors to the ErrorHandler', async () => {
9595
TestBed.configureTestingModule({
9696
providers: [{provide: ErrorHandler, useFactory: () => new FakeErrorHandler()}],
9797
rethrowApplicationErrors: false,
@@ -113,7 +113,7 @@ describe('reactivity', () => {
113113
},
114114
{injector: appRef.injector},
115115
);
116-
appRef.tick();
116+
await appRef.whenStable();
117117
expect(run).toBeTrue();
118118
expect(lastError.message).toBe('fail!');
119119
});

0 commit comments

Comments
 (0)