Skip to content

Commit 2bca907

Browse files
committed
fix(framework): report automatic scheduler flush errors
1 parent f4880e0 commit 2bca907

11 files changed

Lines changed: 288 additions & 15 deletions

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
# Changelog
22

3+
## 0.11.10 - 2026-06-18
4+
5+
- Routed automatic microtask scheduler flush failures through an explicit error
6+
channel so failed jobs no longer surface as unhandled promise rejections.
7+
- Reported automatic flush failures to configured `onError` handlers or
8+
`globalThis.reportError`, with scheduler metadata for phase, scope, and key,
9+
while preserving manual `scheduler.flush()` rejection behavior.
10+
- Added regression coverage for automatic `onError` reporting, `reportError`
11+
delivery without unhandled rejections, and manual flush rejection metadata.
12+
- Bundle size from bundled TypeScript source: `browser.ts` 184,238 B raw /
13+
34,642 B gzip -> `browser.min.js` 78,267 B raw / 23,232 B gzip
14+
(-105,971 B raw, -11,410 B gzip).
15+
316
## 0.11.9 - 2026-06-18
417

518
- Normalized scheduler batch thenables with `Promise.resolve(value)` before

browser.js

Lines changed: 31 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

browser.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

browser.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3240,7 +3240,7 @@ const __schedulerModule = (() => {
32403240
scheduled = true;
32413241
scheduleMicrotask(() => {
32423242
if (!destroyed) {
3243-
void api.flush();
3243+
void api.flush().catch(reportAutomaticFlushError);
32443244
}
32453245
});
32463246
}
@@ -3276,12 +3276,22 @@ const __schedulerModule = (() => {
32763276
if (onError) {
32773277
onError(error, job);
32783278
} else {
3279-
throw error;
3279+
throw annotateSchedulerError(error, job);
32803280
}
32813281
}
32823282
}
32833283
}
32843284

3285+
function reportAutomaticFlushError(error) {
3286+
if (typeof globalThis.reportError === "function") {
3287+
globalThis.reportError(error);
3288+
return;
3289+
}
3290+
setTimeout(() => {
3291+
throw error;
3292+
}, 0);
3293+
}
3294+
32853295
function hasJobs() {
32863296
for (const queue of queues.values()) {
32873297
if (queue.some((job) => !job.canceled)) {
@@ -3340,6 +3350,25 @@ const __schedulerModule = (() => {
33403350
return (typeof scope === "object" && scope !== null) || typeof scope === "function";
33413351
}
33423352

3353+
function annotateSchedulerError(error, job) {
3354+
if (!error || (typeof error !== "object" && typeof error !== "function")) {
3355+
return error;
3356+
}
3357+
try {
3358+
Object.defineProperty(error, "scheduler", {
3359+
configurable: true,
3360+
value: {
3361+
phase: job.phase,
3362+
scope: job.scope,
3363+
key: job.key
3364+
}
3365+
});
3366+
} catch {
3367+
// Non-extensible thrown values still need to propagate through the chosen error channel.
3368+
}
3369+
return error;
3370+
}
3371+
33433372
function scheduleMicrotask(fn) {
33443373
if (typeof queueMicrotask === "function") {
33453374
queueMicrotask(fn);

browser.umd.js

Lines changed: 31 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

browser.umd.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

framework.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3242,7 +3242,7 @@ const __schedulerModule = (() => {
32423242
scheduled = true;
32433243
scheduleMicrotask(() => {
32443244
if (!destroyed) {
3245-
void api.flush();
3245+
void api.flush().catch(reportAutomaticFlushError);
32463246
}
32473247
});
32483248
}
@@ -3278,12 +3278,22 @@ const __schedulerModule = (() => {
32783278
if (onError) {
32793279
onError(error, job);
32803280
} else {
3281-
throw error;
3281+
throw annotateSchedulerError(error, job);
32823282
}
32833283
}
32843284
}
32853285
}
32863286

3287+
function reportAutomaticFlushError(error) {
3288+
if (typeof globalThis.reportError === "function") {
3289+
globalThis.reportError(error);
3290+
return;
3291+
}
3292+
setTimeout(() => {
3293+
throw error;
3294+
}, 0);
3295+
}
3296+
32873297
function hasJobs() {
32883298
for (const queue of queues.values()) {
32893299
if (queue.some((job) => !job.canceled)) {
@@ -3342,6 +3352,25 @@ const __schedulerModule = (() => {
33423352
return (typeof scope === "object" && scope !== null) || typeof scope === "function";
33433353
}
33443354

3355+
function annotateSchedulerError(error, job) {
3356+
if (!error || (typeof error !== "object" && typeof error !== "function")) {
3357+
return error;
3358+
}
3359+
try {
3360+
Object.defineProperty(error, "scheduler", {
3361+
configurable: true,
3362+
value: {
3363+
phase: job.phase,
3364+
scope: job.scope,
3365+
key: job.key
3366+
}
3367+
});
3368+
} catch {
3369+
// Non-extensible thrown values still need to propagate through the chosen error channel.
3370+
}
3371+
return error;
3372+
}
3373+
33453374
function scheduleMicrotask(fn) {
33463375
if (typeof queueMicrotask === "function") {
33473376
queueMicrotask(fn);

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@async/framework",
3-
"version": "0.11.9",
3+
"version": "0.11.10",
44
"description": "No-build Loader app runtime with browser and server entrypoints, signals, command events, route partials, cache split, SSR activation, and streaming boundaries.",
55
"type": "module",
66
"main": "./server.js",

server.js

Lines changed: 31 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/scheduler.js

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ export function createScheduler(options = {}) {
219219
scheduled = true;
220220
scheduleMicrotask(() => {
221221
if (!destroyed) {
222-
void api.flush();
222+
void api.flush().catch(reportAutomaticFlushError);
223223
}
224224
});
225225
}
@@ -255,12 +255,22 @@ export function createScheduler(options = {}) {
255255
if (onError) {
256256
onError(error, job);
257257
} else {
258-
throw error;
258+
throw annotateSchedulerError(error, job);
259259
}
260260
}
261261
}
262262
}
263263

264+
function reportAutomaticFlushError(error) {
265+
if (typeof globalThis.reportError === "function") {
266+
globalThis.reportError(error);
267+
return;
268+
}
269+
setTimeout(() => {
270+
throw error;
271+
}, 0);
272+
}
273+
264274
function hasJobs() {
265275
for (const queue of queues.values()) {
266276
if (queue.some((job) => !job.canceled)) {
@@ -319,6 +329,25 @@ function isObjectScope(scope) {
319329
return (typeof scope === "object" && scope !== null) || typeof scope === "function";
320330
}
321331

332+
function annotateSchedulerError(error, job) {
333+
if (!error || (typeof error !== "object" && typeof error !== "function")) {
334+
return error;
335+
}
336+
try {
337+
Object.defineProperty(error, "scheduler", {
338+
configurable: true,
339+
value: {
340+
phase: job.phase,
341+
scope: job.scope,
342+
key: job.key
343+
}
344+
});
345+
} catch {
346+
// Non-extensible thrown values still need to propagate through the chosen error channel.
347+
}
348+
return error;
349+
}
350+
322351
function scheduleMicrotask(fn) {
323352
if (typeof queueMicrotask === "function") {
324353
queueMicrotask(fn);

0 commit comments

Comments
 (0)