fix(web): handle unhandled promise rejection in CancellableTask (#27553)

When a concurrent caller awaits `this.complete` inside `execute()` and
`cancel()` is called, the promise rejects with `undefined` outside of any
try/catch, causing "Uncaught (in promise) undefined" console spam during
rapid timeline scrolling.

- Wrap the `await this.complete` path in try/catch, returning 'CANCELED'
- Guard the `finally` block to only null `cancelToken` if it still belongs
  to this call, preventing a race condition with `cancel()` to `init()`

Change-Id: I65764dd664eb408433fc6e5fc2be4df56a6a6964
This commit is contained in:
Min Idzelis 2026-04-07 10:22:29 -04:00 committed by GitHub
parent 7f784952eb
commit de9ec95db1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 33 additions and 3 deletions

View File

@ -161,6 +161,30 @@ describe('CancellableTask', () => {
expect(task.executed).toBe(true);
});
it('should return CANCELED when concurrent caller is waiting and task is canceled', async () => {
const task = new CancellableTask();
let resolveTask: () => void;
const taskPromise = new Promise<void>((resolve) => {
resolveTask = resolve;
});
const taskFn = async (signal: AbortSignal) => {
await taskPromise;
if (signal.aborted) {
throw new DOMException('Aborted', 'AbortError');
}
};
const promise1 = task.execute(taskFn, true);
const promise2 = task.execute(taskFn, true);
task.cancel();
resolveTask!();
const [result1, result2] = await Promise.all([promise1, promise2]);
expect(result1).toBe('CANCELED');
expect(result2).toBe('CANCELED');
});
it('should not cancel if task is already executed', async () => {
const task = new CancellableTask();
const taskFn = vi.fn(async () => {});

View File

@ -64,8 +64,12 @@ export class CancellableTask {
if (this.cancellable && !cancellable) {
this.cancellable = cancellable;
}
await this.complete;
return 'WAITED';
try {
await this.complete;
return 'WAITED';
} catch {
return 'CANCELED';
}
}
this.cancellable = cancellable;
const cancelToken = (this.cancelToken = new AbortController());
@ -86,7 +90,9 @@ export class CancellableTask {
this.#transitionToErrored(error);
return 'ERRORED';
} finally {
this.cancelToken = null;
if (this.cancelToken === cancelToken) {
this.cancelToken = null;
}
}
}