Skip to content

Friendlier workflow errors (consolidated)#1849

Draft
pranaygp wants to merge 19 commits intomainfrom
pranaygp/friendlier-errors-followups
Draft

Friendlier workflow errors (consolidated)#1849
pranaygp wants to merge 19 commits intomainfrom
pranaygp/friendlier-errors-followups

Conversation

@pranaygp
Copy link
Copy Markdown
Contributor

@pranaygp pranaygp commented Apr 24, 2026

Summary

Consolidates the eight-PR friendlier-errors stack into a single PR. Inspired by @Schniz's stalled #706. Superseded by this PR: #1831, #1832, #1836, #1837, #1838, #1839, #1840.

What's included

Area Change
@workflow/errors New SerializationError, WorkflowBuildError classes (with optional hint field). Ansi rendering helpers (frame, code, docs, dim, inline) — now lives under the @workflow/errors/ansi subpath so the main entry doesn't pull chalk into every consumer. FatalError.is(err) widened to recognize any error with a fatal: true own property.
@workflow/core — context violations Four structured error classes (NotInWorkflowContextError, NotInStepContextError, NotInWorkflowOrStepContextError, UnavailableInWorkflowContextError) applied to twelve user-facing throw sites. Each includes a docs link. .message / .stack are plain text — the colored framed form renders lazily via [util.inspect.custom] / toString(), so structured logs and log drains no longer contain raw \x1B[...m bytes. All four classes set fatal = true, so createHook()-from-a-step fails immediately instead of burning three retry attempts. Thrown errors redirect their stack to the user's call site via a shared redirectStackToCaller helper so terminal overlays (Next.js, Turbopack, VS Code) point at user code.
@workflow/core — serialization SerializationError applied to all user-facing serialization boundaries: stream locking, unregistered classes, missing WORKFLOW_DESERIALIZE, step-function / workflow-function misuse, and dehydrate/hydrate failures for workflow args, step args, and return values.
@workflow/core — runtime logger Structured logger gains .child() and .forRun(runId, workflowName) for stable per-run context, standardized [workflow-sdk] prefix, error stacks surfaced in log drains, clarified replay-timeout phrasing (warn while retrying vs. error when giving up).
@workflow/core — attribution describeError(err) and describeRunError({ errorCode, errorName }) compute user-vs-SDK attribution + class-aware hints, either from a live Error instance or from persisted failure-event fields. Exposed under the public @workflow/core/describe-error subpath for CLI / web consumption. Terminal logs at step-failure, max-retries, run-failure, and fatal-setup sites now include errorAttribution metadata and hint text.
@workflow/core — consistency Remaining internal invariants (missing startedAt, VM crypto.subtle.generateKey, closure-vars outside a step context, ENOTSUP) now throw WorkflowRuntimeError so they are attributed to the SDK. defineHook().resume() formats schema validation failures as a readable bulleted list instead of a raw JSON dump.
@workflow/builders — build-time Failed esbuild phases, unresolved built-in steps, and empty esbuild output now throw WorkflowBuildError with a hint pointing at the likely fix.

Fixes from manual testing (createHook() inside a step)

Surfaced three issues after running the earlier stack end-to-end and addressed in the final commits:

  1. ANSI bytes were leaking into .message because chalk resolves at construction time — fixed by storing plain text and rendering pretty lazily (see context-errors-plain-message changeset).
  2. Context violations were retrying 3× and producing 4 duplicated log blocks — fixed by marking them fatal: true and widening FatalError.is() (see context-errors-fatal changeset).
  3. Duplicated captureStackTrace feature-detect in two files — fixed by extracting a shared redirectStackToCaller helper (see capture-stack-shared changeset).

Follow-up round (latest commits)

Manual testing uncovered two more issues, fixed in the last two commits on this branch:

  1. SerializationError still looped through 4 retries. Root cause: dehydrateStepReturnValue() was called outside the step-handler try/catch, so FatalError.is() never saw the error. Two-part fix: mark SerializationError as fatal = true, and move the dehydration call inside the user-code try/catch in step-handler.ts so the error routes through userCodeFailedstep_failed (see serialization-error-fatal changeset). Same non-POJO return now fails in ~1.6s / 1 block, not ~21s / 4 blocks.
  2. Snapshot tests for log output. Added toMatchInlineSnapshot-backed tests for describeError() payloads (every attribution path) and the scoped-logger call signature for the two canonical runtime failure sites. Regression gate on the exact field shapes users see in their log drains.

Reviewer reference: pr-artifacts/

Five scenario log captures (01-context-violation… through 05-retryable-error-max-retries) show actual runtime output, before/after framing, and any follow-ups noted during testing. Remove before merge — they're under pr-artifacts/ explicitly so they're easy to git rm in the final prep.

Addressed review feedback

Manual test plan

All sections below exercise different parts of the stack. Start cd workbench/nextjs-turbopack && pnpm dev unless otherwise noted.

1. Context-violation errors (phase 1 + 2 + followups)

A convenient smoke route at workbench/nextjs-turbopack/app/api/friendlier-errors-smoke/route.ts:

import { NextResponse } from 'next/server';
import { createHook, sleep, getStepMetadata, getWorkflowMetadata } from 'workflow';

export async function GET(req: Request) {
  const which = new URL(req.url).searchParams.get('which');
  try {
    if (which === 'createHook') createHook();
    if (which === 'sleep') await sleep('1s');
    if (which === 'getStepMetadata') getStepMetadata();
    if (which === 'getWorkflowMetadata') getWorkflowMetadata();
    return NextResponse.json({ ok: true });
  } catch (err) {
    console.error(err);
    return NextResponse.json(
      { name: (err as Error).name, message: (err as Error).message, stack: (err as Error).stack },
      { status: 500 }
    );
  }
}
  • createHook() outside workflow?which=createHook. Terminal shows a framed box with title `createHook()` can only be called inside a workflow function and a docs: https://…/workflow/create-hook line (polished — no longer note: Read more about…).
  • sleep() outside workflow?which=sleep. Same framing, docs URL ends in .../workflow/sleep.
  • getStepMetadata() in a workflow function (not a step) — add to a "use workflow" file; expect title "can only be called inside a step function".
  • getWorkflowMetadata() in application code?which=getWorkflowMetadata. Expect "workflow or step function".
  • resumeHook() inside a workflow — call resumeHook(token, payload) from inside a "use workflow" function. Title: `resumeHook()` cannot be called from a workflow context, plus a line this call was made from the workflow//./src/workflows/example.ts//myWorkflow workflow context. with the workflow/ prefix dimmed.
  • Stack points at user code — in the JSON response body, the first at ... line of err.stack should reference your route handler, not a frame inside @workflow/core. Same goes for the Next.js dev overlay.
  • No functionName leak — the JSON response body should NOT contain a functionName property on the error object (it used to via the old constructor param-property).
  • ANSI bytes don't leak into .message / .stack (new in this PR) — the JSON response body's message and stack strings must contain no \x1B[ bytes; they're plain text. In the terminal, console.error(err) still renders the pretty framed version via util.inspect.
  • Context violations fail fast (no 4× retries) (new in this PR) — call createHook() inside a "use step" function:
    async function add(a: number, b: number): Promise<number> {
      'use step';
      createHook();
      return a + b;
    }
    Run a workflow that calls add(…). You should see one [workflow-sdk] fatal-error log block (Step "X" threw a FatalError — bubbling up...), not four. The step fails immediately.

2. Runtime logger metadata (phase 3)

  • Standardized prefix — all runtime log lines start with [workflow-sdk]. Grep your terminal output; there should be no [Workflows] or other prefixes.
  • Run context attached without repetition — trigger any workflow; log lines carry workflowRunId and workflowName as metadata fields instead of being baked into the message string.
  • Replay-timeout phrasing:
    • While retries remain: warn level, phrasing includes "took too long — will retry".
    • After final attempt: error level, phrasing includes "gave up".
  • Error stack surfaces in log drain — set up a log drain (or pipe to a file) and confirm the stack is included as a structured field (errorStack), not just the one-line error message.

3. SerializationError (phase 4)

Cause a user-facing serialization failure:

async function stepThatLeaks(): Promise<Map<string, { fn: () => void }>> {
  'use step';
  return new Map([['foo', { fn: () => console.log('unserializable') }]]);
}
  • Run the workflow. Expect [workflow-sdk] log with errorName: 'SerializationError'.
  • errorAttribution: 'user'.
  • hint field present: "A value passed across a workflow/step boundary could not be serialized…".
  • Stream locking: call getWritable('x').getWriter() twice on the same stream — expect a SerializationError with docs link.

4. describeError attribution (phase 5)

For each of these, inspect the [workflow-sdk] log at failure time:

  • Plain user Errorthrow new Error('boom') from a step → errorAttribution: 'user', no hint field.
  • SerializationErrorerrorAttribution: 'user' + serialization hint.
  • Context-violation errorerrorAttribution: 'user' + context hint.
  • WorkflowRuntimeErrorthrow new WorkflowRuntimeError('invariant') from a step → errorAttribution: 'sdk' + runtime hint.
  • Replay timeout — set WORKFLOW_REPLAY_TIMEOUT_MS=50, run a non-trivial workflow → after retries exhaust, errorAttribution: 'sdk' + replay-timeout hint.
  • Max-delivery exhaustion — write a step that always throws; after queue max-delivery budget exhausts → errorAttribution: 'sdk' + max-delivery hint.

5. Consistency pass (phase 6)

  • Zod schema failure on defineHook().resume() — call hook.resume(token, invalidBody). Expect a readable bulleted list of validation issues (one per line, at "field": message), not a raw JSON dump of ZodError.issues.
  • crypto.subtle.generateKey() inside workflow VM — call it from a "use workflow" function. Expect a clear message explaining why it's disabled + "move this into a step function", with errorAttribution: 'sdk'.

6. describeError subpath (phase 7 foundation)

Create scratch.ts at repo root:

import { describeRunError, describeError } from '@workflow/core/describe-error';
import { SerializationError, WorkflowRuntimeError } from '@workflow/errors';

console.log(describeRunError({ errorCode: 'USER_ERROR', errorName: 'SerializationError' }));
console.log(describeRunError({ errorCode: 'USER_ERROR', errorName: 'NotInWorkflowContextError' }));
console.log(describeRunError({ errorCode: 'RUNTIME_ERROR' }));
console.log(describeRunError({ errorCode: 'REPLAY_TIMEOUT' }));
console.log(describeRunError({ errorCode: 'MAX_DELIVERIES_EXCEEDED' }));
console.log(describeRunError({ errorCode: 'USER_ERROR' }));

console.log(describeError(new SerializationError('boom')));
console.log(describeError(new WorkflowRuntimeError('invariant')));
console.log(describeError(new Error('plain')));

Run pnpm tsx scratch.ts.

  • describeRunError({ errorCode: 'USER_ERROR', errorName: 'SerializationError' }){ attribution: 'user', errorCode: 'USER_ERROR', hint: 'A value…serialized…' }.
  • describeRunError({ errorCode: 'RUNTIME_ERROR' }){ attribution: 'sdk', hint: 'This is an internal workflow SDK error…' }.
  • Live-error parity — describeError(new SerializationError('x')) and describeRunError({ errorCode: 'USER_ERROR', errorName: 'SerializationError' }) return the same shape and hint string.
  • Subpath import works — TypeScript resolves @workflow/core/describe-error and pnpm tsx runs without module-resolution errors.

7. WorkflowBuildError (phase 8)

Exercise the build pipeline, not the runtime. Use workbench/nextjs-turbopack and run pnpm build (not pnpm dev).

  • Syntax error in a workflow file → in the build output, expect a WorkflowBuildError titled "Build failed during workflows bundle" followed by a blank line and hint: Review the esbuild errors above…. The original esbuild errors remain printed above (not suppressed).
  • Unresolved built-in stepsmv node_modules/workflow node_modules/workflow-bak and run pnpm build. Expect WorkflowBuildError: Failed to resolve built-in steps sources. + hint: run \pnpm install workflow`…`. Restore afterwards.
  • Empty workflow directory — move all workflow files aside. Expect WorkflowBuildError: No output files generated from esbuild + hint mentioning "use workflow" / "use step" directives.
  • .is() discriminator — in a scratch script: WorkflowBuildError.is(new WorkflowBuildError('x', { hint: 'y' })) returns true; WorkflowBuildError.is(new Error('x')) returns false.
  • Runtime paths unaffected — run a normal workflow at runtime (pnpm dev). Confirm no WorkflowBuildError shows up; this class is build-time only.

8. New @workflow/errors/ansi subpath (final PR)

  • import { Ansi } from '@workflow/errors' no longer works (and wasn't intended to — the helpers were always namespaced). Confirm import * as Ansi from '@workflow/errors/ansi' resolves.
  • A package that imports only error classes (import { SerializationError } from '@workflow/errors') no longer pulls chalk into its bundle. Check a production bundle or pnpm why chalk from a dependent context.

9. FatalError.is() widening (final PR)

  • FatalError.is(new FatalError('x'))true.
  • FatalError.is(new NotInWorkflowContextError('createHook()', 'https://…'))true (via fatal: true own property).
  • FatalError.is(new Error('x'))false.
  • FatalError.is({ fatal: true })false (must be an Error-shaped value).

Unit tests

All packages typecheck clean; relevant test files pass:

  • pnpm --filter @workflow/errors test — 25 tests (Ansi, SerializationError, WorkflowBuildError, new FatalError widening tests)
  • pnpm --filter @workflow/core exec vitest run src/context-errors.test.ts src/describe-error.test.ts — 32 tests (incl. new plain-message / lazy-pretty / FatalError-gate cases)
  • pnpm --filter @workflow/builders test — 129 tests
  • pnpm typecheck — clean across workspace

🤖 Generated with Claude Code

pranaygp and others added 13 commits April 22, 2026 18:28
Phase 1: Add Ansi rendering helpers (frame, hint, note, help, code, inline)
to @workflow/errors, and a chalk mock for readable snapshot tests.

Phase 2: Add four context-violation error classes to @workflow/core
(NotInWorkflowContextError, NotInStepContextError,
NotInWorkflowOrStepContextError, UnavailableInWorkflowContextError)
and apply them to all twelve user-facing throw sites so errors now
include docs links and a structured "what/why/fix" frame.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Tighten phase 1 changeset to a single sentence (per pranaygp review) and switch to double-quoted frontmatter (per Copilot + repo convention).
- Implement `ansifyName` to actually apply dim styling to workflow/ / step/ prefixes; add an `Ansi.dim` helper to `@workflow/errors` so callers don't need to import chalk directly.
- Remove the `void getWorkflowMetadata;` workaround in context-errors.ts by dropping the unused value import (we only needed the type and symbol).
- Render the plain-Error throw in `workflow/get-workflow-metadata.ts` with `Ansi.frame` + docs link so the VM path matches the structured-class styling from the sibling step path (still uses a plain Error to avoid the module-init cycle).
- Guard `buildUnderline` against zero-length markers so a stray empty token can't produce a negative `String.repeat` count.
Adds a `.child()` and `.forRun(runId, workflowName)` child-logger API to
the structured logger so runtime/step code doesn't have to repeat
`workflowRunId`/`workflowName`/`stepId` on every call. Normalizes error
metadata to structured `errorName` / `errorMessage` / `errorStack` fields
instead of ad-hoc `error: err.message` strings, and adds comments to
silent catches that swallow expected idempotency conflicts.

Also folds in the pending changes from #1812 so that PR can be closed:

- Standardize the console prefix to `[workflow-sdk]`.
- Split the replay-timeout log into a warn-while-retrying vs.
  error-when-giving-up, and surface the underlying error when we can't
  mark a timed-out run as failed.
- Include the error stack in the "Fatal runtime error during workflow
  setup" log and in the top-level user-code workflow error log so the
  stack surfaces in flattened log drains.
- Drop the `[Workflows] "<runId>" - ` prefix from
  `buildWorkflowSuspensionMessage` — the structured logger now attaches
  run context.

Supersedes #1812.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
Add describeError() that derives attribution and class-aware hints from
existing error classes + RUN_ERROR_CODES — no event data changes. Wire into
step failures, max-delivery exhaustion, run failures, and fatal setup errors
so terminal logs include errorAttribution and a hint for known error types.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- `describeError(err, errorCode?)` now accepts an optional precomputed
  `RunErrorCode`. `classifyRunError(err)` only narrows to USER_ERROR /
  RUNTIME_ERROR, so the REPLAY_TIMEOUT and MAX_DELIVERIES_EXCEEDED branches
  were previously unreachable from the step / run failure log sites.
  Callers that know the failure category (runtime.ts for replay timeout and
  max-deliveries exhaustion) now pass the code in.
- Context-violation checks use `instanceof` against the actual classes from
  context-errors.ts instead of a name-string set. Type-safe + survives
  class renames.
- Wire the new hints through to the REPLAY_TIMEOUT and MAX_DELIVERIES_EXCEEDED
  log sites so those branches actually render a hint now.
- 3 new tests cover the reachable code paths + precomputed-code override.
- Changeset frontmatter switched to double quotes per repo convention.
Internal invariants now use WorkflowRuntimeError so describeError attributes
them to the SDK: missing startedAt, VM generateKey, closure-vars outside
step context, ENOTSUP. defineHook().resume() formats schema validation
failures as a readable list instead of a JSON blob.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Observability renderers read persisted run_failed / step_failed event data,
not live Error instances. describeRunError takes { errorCode, errorName }
and returns the same { attribution, hint } shape as describeError, so the
CLI and web UI can derive user-vs-SDK framing from the event log directly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add `WorkflowBuildError` class in `@workflow/errors` with optional `hint`
for an actionable next step, and apply it in `@workflow/builders` at
user-facing sites: failed esbuild phases, unresolved built-in steps, and
empty esbuild output now throw `WorkflowBuildError` with a hint pointing
at the likely fix. Runtime invariants remain plain `Error`.
…docs link, redirect stack

- Drop the readonly `functionName` param-property on context-error classes so
  util.inspect no longer prints a trailing `{ functionName: 'foo()' }` block.
- Replace the `DocLink` ("label: https://…") shape with a plain `DocsUrl`
  template-literal type. Error output now renders a single clean line:
  `docs: https://…` (new `Ansi.docs` helper) instead of the noisier
  "note: Read more about foo(): https://…".
- Add throw helpers (`throwNotInWorkflowContext`, etc.) that call
  `Error.captureStackTrace(err, stackStartFn)` on V8 engines so the top frame
  of the thrown error points at the user's call site instead of at the gate
  function inside the framework. Callers pass themselves as the boundary.
- Refactor `defineHook()` (both root and `/workflow`) to use named function
  closures rather than `this.create`/`this.resume`, since the stack redirect
  relies on a stable function identity that survives destructuring.
- Update context-errors.test.ts to snapshot the new `docs:` framing and to
  add a regression test asserting the top stack frame is the user call site.
Copilot AI review requested due to automatic review settings April 24, 2026 02:08
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 24, 2026

🦋 Changeset detected

Latest commit: 9fd914b

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

This PR includes changesets to release 22 packages
Name Type
@workflow/core Patch
@workflow/errors Patch
@workflow/builders Patch
@workflow/utils 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 24, 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 May 1, 2026 1:11am
example-nextjs-workflow-webpack Ready Ready Preview, Comment May 1, 2026 1:11am
example-workflow Ready Ready Preview, Comment May 1, 2026 1:11am
workbench-astro-workflow Ready Ready Preview, Comment May 1, 2026 1:11am
workbench-express-workflow Ready Ready Preview, Comment May 1, 2026 1:11am
workbench-fastify-workflow Ready Ready Preview, Comment May 1, 2026 1:11am
workbench-hono-workflow Ready Ready Preview, Comment May 1, 2026 1:11am
workbench-nitro-workflow Ready Ready Preview, Comment May 1, 2026 1:11am
workbench-nuxt-workflow Ready Ready Preview, Comment May 1, 2026 1:11am
workbench-sveltekit-workflow Ready Ready Preview, Comment May 1, 2026 1:11am
workbench-vite-workflow Ready Ready Preview, Comment May 1, 2026 1:11am
workflow-docs Ready Ready Preview, Comment, Open in v0 May 1, 2026 1:11am
workflow-swc-playground Ready Ready Preview, Comment May 1, 2026 1:11am
workflow-web Ready Ready Preview, Comment May 1, 2026 1:11am

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 24, 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 1054 0 86 1140
✅ 🪟 Windows 95 0 0 95
✅ 📋 Other 267 0 18 285
Total 3522 2 276 3800

❌ 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

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 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
✅ 🪟 Windows
App Passed Failed Skipped
✅ nextjs-turbopack 95 0 0
✅ 📋 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: success
  • Windows: success

Check the workflow run for details.

…e path

SerializationError now carries readonly fatal = true. Step-return
dehydration is wrapped inside the user-code try/catch so that the
resulting error flows through userCodeFailed → step_failed →
FatalError.is() short-circuit instead of bubbling up as HTTP 500 and
triggering a queue retry loop. Retrying a step that returned a non-POJO
is guaranteed to fail the same way, so this saves ~20s and 3 near-
identical error blocks per serialization failure.
Snapshot tests lock in the exact shape of:
- describeError() payloads (attribution, errorCode, hint) for every
  classification — plain Error, SerializationError, context-violation,
  WorkflowRuntimeError, REPLAY_TIMEOUT, MAX_DELIVERIES_EXCEEDED.
- The scoped-logger call signature for the two canonical runtime
  failure paths (fatal-bubble and hit-max-retries), so refactors of
  forRun() / child() metadata merging can't silently change what users
  see in their log drains.

SerializationError now also has a direct test for readonly fatal=true
+ FatalError.is() recognition.

pr-artifacts/ contains real log-output snapshots from running the
nextjs-turbopack workbench against five error scenarios. These are
reference material for reviewers and are flagged to be removed before
merge.
The step-level fatal-error log used to embed the full stack trace inside
an `errorStack` string field in the metadata object, so util.inspect
rendered it as a quote-escaped, line-continuation blob when the log
hit the terminal — unreadable in practice. Move framing + stack into
the log *message* (matching the workflow-level log in runtime.ts) and
keep the metadata object compact with only the indexable structured
fields (`errorAttribution`, `errorName`, `errorMessage`, `hint`,
IDs). Log drains still get the same keys; humans now see a readable
stack trace.

Also introduce `formatStepName` / `formatWorkflowName` in
`@workflow/utils` that render machine names
(`step//./workflows/1_simple//add`) as `add (./workflows/1_simple)` in
log framings, using the existing `parseStepName` / `parseWorkflowName`
parsers. Applied to step-fatal, hit-max-retries, exceeded-max-retries,
and workflow-threw log sites.

Artifacts in pr-artifacts/ updated to show the new output shape, and
renamed .log → .md since they're Markdown and IDE previews are nicer
that way.
Replace util.inspect's default object dump (which quote-escapes
multi-line stacks and paragraph hints into a single-line JSON-y blob)
with a workflow-aware formatter that composes the entire log line
into a single string passed to console.error / console.warn.

Highlights of the new output:
- Per-run / per-step IDs render with their parsed friendly names so
  users see `wrun_… · simple (./workflows/1_simple)` instead of just
  the raw `workflowName: 'workflow//./workflows/1_simple//simple'`.
- Color-coded attribution badge (user error red / sdk error magenta)
  paired with the error class in bold.
- Hints render as a paragraph under `hint:` rather than a backslash-
  `\n`-escaped string.
- Drops redundant fields (errorStack always; errorMessage when it's
  already in the parent message) to avoid double-printing.
- Unknown fields fall through as a sorted `key  value` tail so we
  never silently drop log information.

@workflow/errors/ansi gains bold/red/magenta helpers used by the
formatter. The web / web-shared packages don't consume stderr — they
read structured event payloads from the World event log — so this is
presentation-only at the runtime layer.
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.

2 participants