Phase 4: SerializationError + user-facing serialization hints#1836
Closed
pranaygp wants to merge 2 commits into
Closed
Phase 4: SerializationError + user-facing serialization hints#1836pranaygp wants to merge 2 commits into
pranaygp wants to merge 2 commits into
Conversation
🦋 Changeset detectedLatest commit: 351d330 The changes in this PR will be included in the next version bump. This PR includes changesets to release 21 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Contributor
11 tasks
Contributor
🧪 E2E Test Results❌ Some tests failed Summary
❌ Failed Tests💻 Local Development (2 failed)vite-stable (2 failed):
🐘 Local Postgres (4 failed)express-stable (2 failed):
nitro-stable (2 failed):
Details by Category❌ 💻 Local Development
✅ 📦 Local Production
❌ 🐘 Local Postgres
✅ 📋 Other
❌ Some E2E test jobs failed:
Check the workflow run for details. |
9 tasks
Contributor
Author
|
Stacked: #1837 — Phase 5, presentation-only user vs SDK attribution |
This was referenced Apr 24, 2026
e84e7be to
b2b1587
Compare
Phase 4 of friendlier errors: introduce a `SerializationError` class with
an optional `hint` and a docs link (workflow-sdk.dev/err/serialization-failed),
and adopt it at every user-facing serialization boundary in @workflow/core:
- Locked ReadableStream at a workflow boundary
- Unregistered class / missing `classId` / missing `WORKFLOW_DESERIALIZE`
- Attempting to return step functions to clients or call workflow functions
directly
- Webhook `respondWith()` called outside a step
- `dehydrate*` / `getSerializeStream` failures (workflow args/return, step
args/return, stream chunks)
Internal invariants (format prefix length checks, unknown format bytes,
missing `STREAM_NAME_SYMBOL`, encryption key/size guards, etc.) now throw
`WorkflowRuntimeError` instead of plain `Error` so the classifier and logger
treat them consistently.
`formatSerializationError` now returns `{ message, hint }` so the hint
fragment can be rendered with the standard SerializationError framing
instead of being baked into the message string.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
c954c4c to
351d330
Compare
44 tasks
Contributor
Author
|
Superseded by #1849 — consolidated friendlier-errors PR with all 8 phases + follow-up fixes (ANSI leak, non-retry semantics, shared captureStackTrace helper). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase 4 of the friendlier-errors stack. Introduces
SerializationErrorand adopts it at every user-facing serialization boundary in@workflow/core, while normalizing barethrow new Error(...)internal invariants toWorkflowRuntimeError.What changes
New class
SerializationErrorin@workflow/errors:WorkflowErrorwithslug: 'serialization-failed'so the docs link (https://workflow-sdk.dev/err/serialization-failed) appears in every message.hintoption — an actionable "how to fix" line rendered above the docs link.SerializationError.is(value)for runtime discrimination.User-facing sites throw
SerializationErrorwith a hint:ReadableStreampassed across a workflow boundaryclassId/ unregisteredclassId/ missingWORKFLOW_DESERIALIZEStepFunctionreturned to client /WorkflowFunctioncalled directly /respondWith()outside a stepdehydrate*paths (workflow arguments,workflow return value,step arguments,step return value) and stream-chunk transform errorsformatSerializationErrornow returns{ message, hint }so the hint renders through the standard framing instead of being baked into the message string.Internal invariants converted to
WorkflowRuntimeError: format prefix length checks, unknown serialization format bytes, missingSTREAM_NAME_SYMBOL, encryption key / ciphertext size guards,Stream aborteddefault reason,Writable stream closed prematurely,Failed to get reader, and theWORKFLOW_USE_STEP/ closure-var context checks.Manual test plan
Using
workbench/nextjs-turbopack—pnpm devand watch the terminal. Each test should produce aSerializationErrorwith ahint:line and the docs URLhttps://workflow-sdk.dev/err/serialization-failed.Unregistered class across step boundary:
Expect
SerializationErrornaming"MyThing"and ahint:explaining how to register viaregisterClass/ serde.Non-serializable value (function):
Expect
SerializationErrorwithhint:naming the offending value path (e.g.at .args[0]).Non-serializable value (symbol) — same shape as above but pass
Symbol('x').Locked
ReadableStream— deliberately lock aReadableStream(call.getReader()) before passing it across a step boundary. ExpectSerializationError: locked ReadableStreamwith a hint describing the stream-lock constraint.StepFunctionreturned to client — have a workflow return a bare step reference directly (not a result). Expect a clear message that you can't surface step functions to the caller.WorkflowFunctioncalled directly — call a"use workflow"-tagged function from regular app code (not throughstart()). Expect a clear message pointing atstart().respondWith()outside a step — callrespondWith(new Response())from workflow or application code. Expect aSerializationErrorexplaining thatrespondWithonly works inside step functions.Stream chunk transform error — pipe a non-serializable value through
getWritable(). Expect aSerializationErroridentifying the offending chunk.Internal invariant now attributed to SDK — harder to induce, but if you can corrupt a serialization prefix byte or strip
STREAM_NAME_SYMBOL, confirm the thrown error isWorkflowRuntimeError(not bareError) so Phase 5'sdescribeErrorattributes it tosdk.Attribution in step-failed log — confirm the
[workflow-sdk]log at step-failure time carrieserrorAttribution: 'user'for all the above, plushint: 'A value passed across a workflow/step boundary…'(Phase 5 wiring).Unit tests
pnpm --filter @workflow/errors test— 15 pass (10 Ansi + 5 newSerializationError)pnpm --filter @workflow/core exec vitest run src/serialization.test.ts— 116 pass; 7 pre-existingDOMExceptionfailures unrelated (confirmed on stash baseline)"should throw error when reviver cannot find registered step function"for the new hint text📚 Friendlier errors stack
Multi-PR initiative inspired by @Schniz's stalled #706:
Ansirendering primitives + context-violation errorsSerializationErrorat serialization / stream / encryption boundariesdescribeError)throw new Error(...)sitesdescribeRunError+ public subpathWorkflowBuildError+ applications in@workflow/buildersfunctionNameleak, simplify docs framing, redirect stack to user codeEach PR is stacked on the previous one; merge in order.
🤖 Generated with Claude Code