Skip to content

Commit 7232ce5

Browse files
atscottmmalerba
authored andcommitted
fix(core): Catch and report rejections in async function of PendingTasks.run (#60044)
This commit ensures that rejections of the promise of the async function passed to `PendingTasks.run` are not dangling and get reported to the application error handler. This prevents what would likely be a common dangling promise that could end up crashing the node process. BREAKING CHANGE: `PendingTasks.run` no longer returns the result of the async function. If this behavior is desired, it can be re-implemented manually with the `PendingTasks.add`. Be aware, however, that promise rejections will need to be handled or they can cause the node process to shut down when using SSR. PR Close #60044
1 parent d9af1bb commit 7232ce5

File tree

5 files changed

+26
-46
lines changed

5 files changed

+26
-46
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1396,7 +1396,7 @@ export const PACKAGE_ROOT_URL: InjectionToken<string>;
13961396
// @public
13971397
export class PendingTasks {
13981398
add(): () => void;
1399-
run<T>(fn: () => Promise<T>): Promise<T>;
1399+
run<T>(fn: () => Promise<T>): void;
14001400
// (undocumented)
14011401
static ɵprov: unknown;
14021402
}

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ 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';
3435

3536
@Injectable({providedIn: 'root'})
3637
export class NgZoneChangeDetectionScheduler {
@@ -124,6 +125,14 @@ export function internalProvideZoneChangeDetection({
124125
provide: SCHEDULE_IN_ROOT_ZONE,
125126
useValue: scheduleInRootZone ?? SCHEDULE_IN_ROOT_ZONE_DEFAULT,
126127
},
128+
{
129+
provide: INTERNAL_APPLICATION_ERROR_HANDLER,
130+
useFactory: () => {
131+
const zone = inject(NgZone);
132+
const userErrorHandler = inject(ErrorHandler);
133+
return (e: unknown) => zone.runOutsideAngular(() => userErrorHandler.handleError(e));
134+
},
135+
},
127136
];
128137
}
129138

packages/core/src/error_handler.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
*/
88

99
import {inject, InjectionToken} from './di';
10-
import {NgZone} from './zone';
1110

1211
/**
1312
* Provides a hook for centralized exception handling.
@@ -53,18 +52,14 @@ export class ErrorHandler {
5352

5453
/**
5554
* `InjectionToken` used to configure how to call the `ErrorHandler`.
56-
*
57-
* `NgZone` is provided by default today so the default (and only) implementation for this
58-
* is calling `ErrorHandler.handleError` outside of the Angular zone.
5955
*/
6056
export const INTERNAL_APPLICATION_ERROR_HANDLER = new InjectionToken<(e: any) => void>(
6157
typeof ngDevMode === 'undefined' || ngDevMode ? 'internal error handler' : '',
6258
{
6359
providedIn: 'root',
6460
factory: () => {
65-
const zone = inject(NgZone);
6661
const userErrorHandler = inject(ErrorHandler);
67-
return (e: unknown) => zone.runOutsideAngular(() => userErrorHandler.handleError(e));
62+
return (e: unknown) => userErrorHandler.handleError(e);
6863
},
6964
},
7065
);

packages/core/src/pending_tasks.ts

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
ChangeDetectionScheduler,
1616
NotificationSource,
1717
} from './change_detection/scheduling/zoneless_scheduling';
18+
import {INTERNAL_APPLICATION_ERROR_HANDLER} from './error_handler';
1819

1920
/**
2021
* Internal implementation of the pending tasks service.
@@ -85,8 +86,9 @@ export class PendingTasksInternal implements OnDestroy {
8586
* @developerPreview
8687
*/
8788
export class PendingTasks {
88-
private internalPendingTasks = inject(PendingTasksInternal);
89-
private scheduler = inject(ChangeDetectionScheduler);
89+
private readonly internalPendingTasks = inject(PendingTasksInternal);
90+
private readonly scheduler = inject(ChangeDetectionScheduler);
91+
private readonly errorHandler = inject(INTERNAL_APPLICATION_ERROR_HANDLER);
9092
/**
9193
* Adds a new task that should block application's stability.
9294
* @returns A cleanup function that removes a task when called.
@@ -114,23 +116,11 @@ export class PendingTasks {
114116
* });
115117
* ```
116118
*
117-
* Application stability is at least delayed until the next tick after the `run` method resolves
118-
* so it is safe to make additional updates to application state that would require UI synchronization:
119-
*
120-
* ```ts
121-
* const userData = await pendingTasks.run(() => fetch('/api/user'));
122-
* this.userData.set(userData);
123-
* ```
124-
*
125119
* @param fn The asynchronous function to execute
126120
*/
127-
async run<T>(fn: () => Promise<T>): Promise<T> {
121+
run<T>(fn: () => Promise<T>): void {
128122
const removeTask = this.add();
129-
try {
130-
return await fn();
131-
} finally {
132-
removeTask();
133-
}
123+
fn().catch(this.errorHandler).finally(removeTask);
134124
}
135125

136126
/** @nocollapse */

packages/core/test/acceptance/pending_tasks_spec.ts

Lines changed: 9 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,10 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import {ApplicationRef, PendingTasks} from '@angular/core';
9+
import {ApplicationRef, PendingTasks, ErrorHandler} from '@angular/core';
1010
import {TestBed} from '@angular/core/testing';
1111
import {EMPTY, firstValueFrom, of} from 'rxjs';
12-
import {filter, map, take, withLatestFrom} from 'rxjs/operators';
12+
import {map, withLatestFrom} from 'rxjs/operators';
1313

1414
import {PendingTasksInternal} from '../../src/pending_tasks';
1515

@@ -96,36 +96,22 @@ describe('public PendingTasks', () => {
9696
await expectAsync(TestBed.inject(ApplicationRef).whenStable()).toBeResolved();
9797
});
9898

99-
it('should return the result of the run function', async () => {
100-
const appRef = TestBed.inject(ApplicationRef);
101-
const pendingTasks = TestBed.inject(PendingTasks);
102-
103-
const result = await pendingTasks.run(async () => {
104-
await expectAsync(applicationRefIsStable(appRef)).toBeResolvedTo(false);
105-
return 1;
106-
});
107-
108-
expect(result).toBe(1);
109-
await expectAsync(applicationRefIsStable(appRef)).toBeResolvedTo(false);
110-
await expectAsync(TestBed.inject(ApplicationRef).whenStable()).toBeResolved();
111-
});
112-
113-
xit('should stop blocking stability if run promise rejects', async () => {
99+
it('should stop blocking stability if run promise rejects', async () => {
114100
const appRef = TestBed.inject(ApplicationRef);
115101
const pendingTasks = TestBed.inject(PendingTasks);
102+
const errorHandler = TestBed.inject(ErrorHandler);
103+
const spy = spyOn(errorHandler, 'handleError');
116104

117105
let rejectFn: () => void;
118-
const task = pendingTasks.run(() => {
106+
pendingTasks.run(() => {
119107
return new Promise<void>((_, reject) => {
120108
rejectFn = reject;
121109
});
122110
});
123111
await expectAsync(applicationRefIsStable(appRef)).toBeResolvedTo(false);
124-
try {
125-
rejectFn!();
126-
await task;
127-
} catch {}
128-
await expectAsync(applicationRefIsStable(appRef)).toBeResolvedTo(true);
112+
rejectFn!();
113+
await expectAsync(appRef.whenStable()).toBeResolved();
114+
expect(spy).toHaveBeenCalled();
129115
});
130116
});
131117

0 commit comments

Comments
 (0)