Skip to content

Phase 4: SerializationError + user-facing serialization hints#1836

Closed
pranaygp wants to merge 2 commits into
pranaygp/friendlier-errors-phase-3-loggerfrom
pranaygp/friendlier-errors-phase-4-serialization
Closed

Phase 4: SerializationError + user-facing serialization hints#1836
pranaygp wants to merge 2 commits into
pranaygp/friendlier-errors-phase-3-loggerfrom
pranaygp/friendlier-errors-phase-4-serialization

Conversation

@pranaygp
Copy link
Copy Markdown
Contributor

@pranaygp pranaygp commented Apr 23, 2026

Summary

Phase 4 of the friendlier-errors stack. Introduces SerializationError and adopts it at every user-facing serialization boundary in @workflow/core, while normalizing bare throw new Error(...) internal invariants to WorkflowRuntimeError.

What changes

  • New class SerializationError in @workflow/errors:

    • Extends WorkflowError with slug: 'serialization-failed' so the docs link (https://workflow-sdk.dev/err/serialization-failed) appears in every message.
    • Optional hint option — an actionable "how to fix" line rendered above the docs link.
    • SerializationError.is(value) for runtime discrimination.
  • User-facing sites throw SerializationError with a hint:

    • Locked ReadableStream passed across a workflow boundary
    • Class without classId / unregistered classId / missing WORKFLOW_DESERIALIZE
    • StepFunction returned to client / WorkflowFunction called directly / respondWith() outside a step
    • All four dehydrate* paths (workflow arguments, workflow return value, step arguments, step return value) and stream-chunk transform errors
    • formatSerializationError now 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, missing STREAM_NAME_SYMBOL, encryption key / ciphertext size guards, Stream aborted default reason, Writable stream closed prematurely, Failed to get reader, and the WORKFLOW_USE_STEP / closure-var context checks.

Manual test plan

Using workbench/nextjs-turbopackpnpm dev and watch the terminal. Each test should produce a SerializationError with a hint: line and the docs URL https://workflow-sdk.dev/err/serialization-failed.

  • Unregistered class across step boundary:

    class MyThing { constructor(public x: number) {} }
    
    async function myStep(thing: MyThing) { 'use step'; return thing.x; }
    
    export async function wf() {
      'use workflow';
      await myStep(new MyThing(1));
    }

    Expect SerializationError naming "MyThing" and a hint: explaining how to register via registerClass / serde.

  • Non-serializable value (function):

    async function echo<T>(v: T) { 'use step'; return v; }
    
    export async function wf() {
      'use workflow';
      await echo(() => 123); // function is not serializable
    }

    Expect SerializationError with hint: 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 a ReadableStream (call .getReader()) before passing it across a step boundary. Expect SerializationError: locked ReadableStream with a hint describing the stream-lock constraint.

  • StepFunction returned 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.

  • WorkflowFunction called directly — call a "use workflow"-tagged function from regular app code (not through start()). Expect a clear message pointing at start().

  • respondWith() outside a step — call respondWith(new Response()) from workflow or application code. Expect a SerializationError explaining that respondWith only works inside step functions.

  • Stream chunk transform error — pipe a non-serializable value through getWritable(). Expect a SerializationError identifying 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 is WorkflowRuntimeError (not bare Error) so Phase 5's describeError attributes it to sdk.

  • Attribution in step-failed log — confirm the [workflow-sdk] log at step-failure time carries errorAttribution: 'user' for all the above, plus hint: 'A value passed across a workflow/step boundary…' (Phase 5 wiring).

Unit tests

  • pnpm --filter @workflow/errors test — 15 pass (10 Ansi + 5 new SerializationError)
  • pnpm --filter @workflow/core exec vitest run src/serialization.test.ts — 116 pass; 7 pre-existing DOMException failures unrelated (confirmed on stash baseline)
  • Updated "should throw error when reviver cannot find registered step function" for the new hint text
  • Typecheck clean for Phase 4 changes

📚 Friendlier errors stack

Multi-PR initiative inspired by @Schniz's stalled #706:

# PR Phase Summary
1 #1831 Phase 1 + 2 Ansi rendering primitives + context-violation errors
2 #1832 Phase 3 Structured logger metadata; folds in #1812
3 → this PR (#1836) Phase 4 SerializationError at serialization / stream / encryption boundaries
4 #1837 Phase 5 Presentation-only user vs SDK attribution (describeError)
5 #1838 Phase 6 Consistency pass on remaining bare throw new Error(...) sites
6 #1839 Phase 7 foundation Data-driven describeRunError + public subpath
7 #1840 Phase 8 WorkflowBuildError + applications in @workflow/builders
8 #1849 Followups Drop functionName leak, simplify docs framing, redirect stack to user code

Each PR is stacked on the previous one; merge in order.

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings April 23, 2026 16:34
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 23, 2026

🦋 Changeset detected

Latest commit: 351d330

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 21 packages
Name Type
@workflow/core Patch
@workflow/errors Patch
@workflow/builders Patch
@workflow/cli Patch
@workflow/next Patch
@workflow/nitro Patch
@workflow/vitest Patch
@workflow/web-shared Patch
@workflow/web Patch
workflow Patch
@workflow/world-testing Patch
@workflow/world-local Patch
@workflow/world-postgres Patch
@workflow/world-vercel Patch
@workflow/astro Patch
@workflow/nest Patch
@workflow/rollup Patch
@workflow/sveltekit Patch
@workflow/vite Patch
@workflow/nuxt Patch
@workflow/ai Patch

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

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Apr 23, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
example-nextjs-workflow-turbopack Ready Ready Preview, Comment Apr 24, 2026 1:19am
example-nextjs-workflow-webpack Ready Ready Preview, Comment Apr 24, 2026 1:19am
example-workflow Ready Ready Preview, Comment Apr 24, 2026 1:19am
workbench-astro-workflow Ready Ready Preview, Comment Apr 24, 2026 1:19am
workbench-express-workflow Ready Ready Preview, Comment Apr 24, 2026 1:19am
workbench-fastify-workflow Ready Ready Preview, Comment Apr 24, 2026 1:19am
workbench-hono-workflow Ready Ready Preview, Comment Apr 24, 2026 1:19am
workbench-nitro-workflow Ready Ready Preview, Comment Apr 24, 2026 1:19am
workbench-nuxt-workflow Ready Ready Preview, Comment Apr 24, 2026 1:19am
workbench-sveltekit-workflow Ready Ready Preview, Comment Apr 24, 2026 1:19am
workbench-vite-workflow Ready Ready Preview, Comment Apr 24, 2026 1:19am
workflow-docs Ready Ready Preview, Comment, Open in v0 Apr 24, 2026 1:19am
workflow-swc-playground Ready Ready Preview, Comment Apr 24, 2026 1:19am
workflow-web Ready Ready Preview, Comment Apr 24, 2026 1:19am

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 23, 2026

🧪 E2E Test Results

Some tests failed

Summary

Passed Failed Skipped Total
❌ 💻 Local Development 1052 2 86 1140
✅ 📦 Local Production 1054 0 86 1140
❌ 🐘 Local Postgres 1050 4 86 1140
✅ 📋 Other 267 0 18 285
Total 3423 6 276 3705

❌ Failed Tests

💻 Local Development (2 failed)

vite-stable (2 failed):

  • error handling error propagation step errors basic step error preserves message and stack trace
  • error handling error propagation step errors cross-file step error preserves message and function names in stack
🐘 Local Postgres (4 failed)

express-stable (2 failed):

  • fibonacciWorkflow - recursive workflow composition via start()
  • health check (queue-based) - workflow and step endpoints respond to health check messages

nitro-stable (2 failed):

  • fibonacciWorkflow - recursive workflow composition via start()
  • health check (queue-based) - workflow and step endpoints respond to health check messages

Details by Category

❌ 💻 Local Development
App Passed Failed Skipped
✅ astro-stable 89 0 6
✅ express-stable 89 0 6
✅ fastify-stable 89 0 6
✅ hono-stable 89 0 6
✅ nextjs-turbopack-canary 76 0 19
✅ nextjs-turbopack-stable 95 0 0
✅ nextjs-webpack-canary 76 0 19
✅ nextjs-webpack-stable 95 0 0
✅ nitro-stable 89 0 6
✅ nuxt-stable 89 0 6
✅ sveltekit-stable 89 0 6
❌ vite-stable 87 2 6
✅ 📦 Local Production
App Passed Failed Skipped
✅ astro-stable 89 0 6
✅ express-stable 89 0 6
✅ fastify-stable 89 0 6
✅ hono-stable 89 0 6
✅ nextjs-turbopack-canary 76 0 19
✅ nextjs-turbopack-stable 95 0 0
✅ nextjs-webpack-canary 76 0 19
✅ nextjs-webpack-stable 95 0 0
✅ nitro-stable 89 0 6
✅ nuxt-stable 89 0 6
✅ sveltekit-stable 89 0 6
✅ vite-stable 89 0 6
❌ 🐘 Local Postgres
App Passed Failed Skipped
✅ astro-stable 89 0 6
❌ express-stable 87 2 6
✅ fastify-stable 89 0 6
✅ hono-stable 89 0 6
✅ nextjs-turbopack-canary 76 0 19
✅ nextjs-turbopack-stable 95 0 0
✅ nextjs-webpack-canary 76 0 19
✅ nextjs-webpack-stable 95 0 0
❌ nitro-stable 87 2 6
✅ nuxt-stable 89 0 6
✅ sveltekit-stable 89 0 6
✅ vite-stable 89 0 6
✅ 📋 Other
App Passed Failed Skipped
✅ e2e-local-dev-nest-stable 89 0 6
✅ e2e-local-postgres-nest-stable 89 0 6
✅ e2e-local-prod-nest-stable 89 0 6

📋 View full workflow run


Some E2E test jobs failed:

  • Vercel Prod: failure
  • Local Dev: failure
  • Local Prod: success
  • Local Postgres: failure
  • Windows: cancelled

Check the workflow run for details.

@pranaygp
Copy link
Copy Markdown
Contributor Author

Stacked: #1837 — Phase 5, presentation-only user vs SDK attribution

pranaygp and others added 2 commits April 23, 2026 18:15
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>
@pranaygp
Copy link
Copy Markdown
Contributor Author

Superseded by #1849 — consolidated friendlier-errors PR with all 8 phases + follow-up fixes (ANSI leak, non-retry semantics, shared captureStackTrace helper).

@pranaygp pranaygp closed this Apr 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant