DEV Community

Scarab Systems
Scarab Systems

Posted on

Scarab Diagnostic Field Test #039 - SvelteKit Streamed Data Rejection Timing Boundary

Target: sveltejs/kit
Issue: sveltejs/kit#9785
PR: sveltejs/kit#16268
Field Lab: https://github.com/scarab-systems/scarab-field-lab/tree/main/field-tests/sveltejs-kit-9785

This field test targeted a SvelteKit streamed data bug where a rejected streamed promise could become an unhandled promise rejection instead of reaching the page-level {#await ... {:catch ...}} handler.

The visible issue was simple:
a server load returned streamed data
one streamed promise rejected
another server load delayed data serialization
the streamed promise could reject before SvelteKit attached the stream rejection handler
the page catch block did not reliably receive the error
That is a small timing surface, but it matters because streamed data is part of SvelteKit’s server-to-client contract.
A framework does not get to be “almost right” when it owns an error boundary. If a streamed promise is supposed to be represented in page data, the framework needs to attach rejection handling before the promise can escape as an unhandled rejection.
The diagnostic question here was not:
How do we catch this error everywhere?
The better question was:
Where does SvelteKit attach rejection handling for streamed server data, and can delayed load serialization leave a streamed promise unprotected?

Field Lab record

The public case record for this field test is available in the Scarab Field Lab:
https://github.com/scarab-systems/scarab-field-lab/tree/main/field-tests/sveltejs-kit-9785

SDS result

This field test was run in SDS field-test posture against SvelteKit.
The useful diagnostic output was not a broad rendering rewrite recommendation.
It was a boundary read.
The failure lived in a very specific order:
server load returns streamed data
another server load remains pending
JSON data serialization is delayed
stream rejection handling is not attached yet
the streamed promise rejects before the handler exists
That chain left very little room for a broad client-side patch.
The right repair was not to change Svelte’s {#await} behavior.
It was not to redesign SvelteKit streaming.
It was not to wrap every load promise with generic rejection handling.
It was to move JSON data serializer creation earlier and add each server load result to the serializer as that load resolves, while still waiting for all loads before emitting the final response.

Failure shape

The failure shape was a timing/order mismatch.
The application expected streamed rejection to become page-observable data.
SvelteKit’s serializer had the mechanism to attach stream rejection handling.
But delayed loads could postpone serializer use long enough for a streamed promise to reject first.
In plain English:
the framework had the right handler, but attached it too late in one timing shape
That is not a client rendering problem.
That is not a user configuration problem.
That is not a general promise problem.
That is a server data serialization boundary problem.
SvelteKit owns the translation from server load results into serialized page data. The stream rejection handler belongs at that translation boundary.

Boundary

The boundary here is:
streamed server-data promise lifecycle versus final data-response assembly
Those are related, but they are not the same responsibility.
The final response still needs to preserve response shape and ordering.
But streamed promises need rejection handlers as soon as their server load result is available.
The bug appeared because those responsibilities were coupled too tightly.
If SvelteKit waits for all server loads before attaching stream handlers, a streamed promise from an earlier-resolved load can reject while an unrelated load is still pending.
So the repair stayed inside SvelteKit’s server data serialization path.
It did not reinterpret page error handling.
It did not alter the client runtime.
It did not change the public response envelope.
It repaired the timing boundary where streamed data becomes serialized data.
That is the Scarab boundary:
preserve the public response contract, but attach stream lifecycle handling at the earliest safe serialization point

What changed

The PR updates SvelteKit server data serialization so the JSON data serializer is created earlier.
Each server load result is added to the serializer as that load resolves.
The final response still waits for all load promises before emitting data.
That means response shape and order are preserved.
But streamed-data rejection handlers are attached earlier.
The changed public files are:


text
.changeset/quiet-masks-shop.md
packages/kit/src/runtime/server/data/index.js
packages/kit/test/apps/basics/src/routes/streaming/+page.svelte
packages/kit/test/apps/basics/src/routes/streaming/server/delayed-rejection/+layout.server.js
packages/kit/test/apps/basics/src/routes/streaming/server/delayed-rejection/+page.server.js
packages/kit/test/apps/basics/src/routes/streaming/server/delayed-rejection/+page.svelte
packages/kit/test/apps/basics/test/client.test.js

The patch adds a focused regression route where one server load delays serialization and another returns a rejected streamed promise.

It also adds a Playwright regression test verifying that the eager value renders and the streamed rejection reaches the page catch block as:

delayed rejection (500 Internal Error)

Together, those tests cover the timing shape that exposed the bug and the user-visible recovery path.

Why this was not a broad streaming rewrite

The tempting mistake in framework bugs is to make the patch bigger than the failure.

Because SvelteKit coordinates server loads, serialized data, streaming, client navigation, and page rendering, a rejected promise can appear to invite a large abstraction change.

That would have been the wrong move here.

The reported failure did not prove that SvelteKit streaming was globally broken.

It did not prove that Svelte {#await} handling was wrong.

It did not prove that page rendering needed a new error model.

It proved one narrower thing:

a streamed promise could reject before SvelteKit attached the rejection handler that serializes that error into page data

So the patch stayed at that level.

That is the discipline of this kind of repair.

When the failure is a timing boundary, the patch should move the boundary — not redesign the whole subsystem.

Why the diagnostic result mattered

This case is useful because the patch is small, but the boundary is important.

In a framework, server-to-client data correctness depends on ordering decisions.

A handler attached one step too late can be the difference between a page-level catch block and a server-side unhandled rejection.

SDS helped keep the repair framed around the actual ownership surface:

SvelteKit owns server data serialization.

The JSON data serializer owns streamed-data rejection handling.

A streamed promise should not be allowed to reject before that serializer has attached the handler.

That framing kept the repair from drifting into unrelated client rendering, generic promise handling, or broad streaming behavior.

The result was a small patch with direct evidence.

Validation

The patch was validated with focused checks before submission.

Validation passed for:

Prettier check on touched files

pnpm --dir packages/kit check

pnpm --dir packages/kit/test/apps/basics check

pnpm run lint

pnpm run check

focused Playwright regression in dev mode

focused Playwright regression in build/preview mode

A reduced full pnpm test:kit run was also attempted.

The new regression passed inside that run, but the full command failed on an existing unrelated no-SSR dev test. This report does not claim the full local suite passed.

At the time of this report, the PR is open and ready for review.

Several public CI checks had already passed, several were still pending, and no failed GitHub checks were visible in the latest public check output.

For the full validation record and public status, see the Field Lab case record:

https://github.com/scarab-systems/scarab-field-lab/tree/main/field-tests/sveltejs-kit-9785

Field test result

This was a bounded SvelteKit streamed-data serialization timing repair candidate.

The issue reduced to:

a server load returned streamed data

another server load delayed final data serialization

the streamed promise could reject before the serializer attached rejection handling

the rejection could become unhandled instead of reaching page-level catch handling

SvelteKit already had stream rejection serialization logic

the timing boundary allowed that logic to attach too late

the PR creates the serializer earlier and adds each load result as it resolves

the final response still waits for all loads, preserving response shape and order

the PR adds a focused delayed-load streamed rejection regression

That is the repair lane.

This patch does not claim to redesign SvelteKit streaming.

It does not claim to change Svelte client rendering.

It does not claim to alter the public response envelope.

It fixes the timing boundary where streamed server data becomes serialized page data.

Public claim

The correct claim for this field test is:

Scarab/SDS helped drive a bounded repair candidate for sveltejs/kit#9785, where a rejected streamed data promise could occur before SvelteKit attached stream rejection handling if another server load delayed serialization. The upstream PR creates the JSON data serializer earlier, adds each load result as it resolves, and still waits for all loads before emitting the final response. The repair includes a focused delayed-load streamed rejection route and Playwright regression coverage. Validation passed through formatting checks, package checks, lint, repository check, and focused Playwright regressions in both dev and build/preview modes. This does not claim to redesign SvelteKit streaming or client rendering broadly; it fixes the serialization timing boundary that allowed rejected streamed data to escape before page-level catch handling.

Disclosure: This field report was prepared with AI-assisted editing from my own field-test notes, public issue and PR records, validation summary, and repair record. The technical claims and final wording were reviewed before publication.
Enter fullscreen mode Exit fullscreen mode

Top comments (0)