Skip to content

Data-driven describeRunError + expose via @workflow/core/describe-error#1839

Closed
pranaygp wants to merge 1 commit intopranaygp/friendlier-errors-phase-6-consistencyfrom
pranaygp/friendlier-errors-phase-7-observability
Closed

Data-driven describeRunError + expose via @workflow/core/describe-error#1839
pranaygp wants to merge 1 commit intopranaygp/friendlier-errors-phase-6-consistencyfrom
pranaygp/friendlier-errors-phase-7-observability

Conversation

@pranaygp
Copy link
Copy Markdown
Contributor

@pranaygp pranaygp commented Apr 23, 2026

Summary

Phase 7 foundation of the friendlier-errors stack. Adds a data-driven describeRunError alongside the live-Error describeError, and exposes both under a public @workflow/core/describe-error subpath so observability renderers (CLI, web UI) can use them without taking on the entire core runtime.

  • describeRunError({ errorCode, errorName }) — derives the same { attribution, hint, errorCode } shape as describeError(err), but from persisted run_failed / step_failed event fields. After hydration, renderers only have these primitives — not the original class instance — so attribution needs a data-driven path.
  • Shared hint strings — all six hint strings are now module-level constants, reused by both helpers, so terminal logs and UI banners render identical wording.
  • New ./describe-error subpath on @workflow/core so the CLI / web packages can consume these helpers without depending on the runtime entry point.
  • Tests: existing 6 live-Error tests kept; 8 new tests cover the persisted-data matrix (plain user error, SerializationError, context-violation, WorkflowRuntimeError, StepNotRegisteredError-by-name, REPLAY_TIMEOUT, MAX_DELIVERIES_EXCEEDED, missing errorCode fallback).

Scope note: deliberately scoped to the shared helper + public surface so it can merge cleanly. The UI wiring (split SDK / user banners, humanized step names, screenshot regression case) will follow separately on top of this.

Manual test plan

This PR is mostly a foundation that other renderers will consume. Test it by importing the subpath in a scratch script — no workbench needed.

Create a scratch file at the repo root:

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

// describeRunError — data-driven path (from persisted fields)
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' })); // plain user, no hint
console.log(describeRunError({ errorCode: 'SOMETHING_WEIRD' })); // unknown → fallback

// describeError — live-Error path (existing API)
console.log(describeError(new SerializationError('boom')));
console.log(describeError(new WorkflowRuntimeError('invariant')));
console.log(describeError(new Error('plain')));

Run with pnpm tsx scratch.ts.

  • SerializationError by name{ attribution: 'user', errorCode: 'USER_ERROR', hint: 'A value…serialized…' }
  • Context-violation by name{ attribution: 'user', hint: 'A workflow-only or step-only API…' }
  • RUNTIME_ERROR code{ attribution: 'sdk', hint: 'This is an internal workflow SDK error…' }
  • REPLAY_TIMEOUT code{ attribution: 'sdk', hint: 'The workflow replay took too long…' }
  • MAX_DELIVERIES_EXCEEDED code{ attribution: 'sdk', hint: 'The workflow queue exceeded its max-delivery budget…' }
  • Plain USER_ERROR (no errorName) → { attribution: 'user', errorCode: 'USER_ERROR' } with no hint field.
  • Unknown errorCode falls through{ attribution: 'user', errorCode: 'USER_ERROR' } (normalized).
  • Live-error path paritydescribeError(new SerializationError('x')) and describeRunError({ errorCode: 'USER_ERROR', errorName: 'SerializationError' }) return the same shape and same hint string (module-level constants should be shared).
  • Subpath import works — TypeScript resolves @workflow/core/describe-error and pnpm tsx runs without module-resolution errors. This verifies the new export in package.json / dist/ layout.

Unit tests

  • pnpm --filter @workflow/core exec vitest run src/describe-error.test.ts (14 tests)
  • dist/describe-error.js / dist/describe-error.d.ts emitted by existing build pipeline

📚 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 #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 → this PR (#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

@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:27am
example-nextjs-workflow-webpack Ready Ready Preview, Comment Apr 24, 2026 1:27am
example-workflow Ready Ready Preview, Comment Apr 24, 2026 1:27am
workbench-astro-workflow Ready Ready Preview, Comment Apr 24, 2026 1:27am
workbench-express-workflow Ready Ready Preview, Comment Apr 24, 2026 1:27am
workbench-fastify-workflow Ready Ready Preview, Comment Apr 24, 2026 1:27am
workbench-hono-workflow Ready Ready Preview, Comment Apr 24, 2026 1:27am
workbench-nitro-workflow Ready Ready Preview, Comment Apr 24, 2026 1:27am
workbench-nuxt-workflow Ready Ready Preview, Comment Apr 24, 2026 1:27am
workbench-sveltekit-workflow Ready Ready Preview, Comment Apr 24, 2026 1:27am
workbench-vite-workflow Ready Ready Preview, Comment Apr 24, 2026 1:27am
workflow-docs Ready Ready Preview, Comment, Open in v0 Apr 24, 2026 1:27am
workflow-swc-playground Ready Ready Preview, Comment Apr 24, 2026 1:27am
workflow-web Ready Ready Preview, Comment Apr 24, 2026 1:27am

@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 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.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 23, 2026

🦋 Changeset detected

Latest commit: e6b8e31

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

This PR includes changesets to release 17 packages
Name Type
@workflow/core 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/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

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a data-driven error description helper to complement the existing live-Error describeError, and exposes these helpers via a new public @workflow/core/describe-error subpath for observability consumers (CLI/UI) to render consistent attribution/hints without depending on the full runtime entrypoint.

Changes:

  • Introduces describeRunError({ errorCode, errorName }) and shared hint string constants in packages/core/src/describe-error.ts.
  • Expands unit tests to cover the persisted-signal matrix for describeRunError.
  • Exposes a new ./describe-error export in @workflow/core and adds a changeset.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
packages/core/src/describe-error.ts Adds describeRunError and refactors shared hint strings for consistent attribution/hints.
packages/core/src/describe-error.test.ts Adds test coverage for the new data-driven helper.
packages/core/package.json Exposes @workflow/core/describe-error subpath export for consumers.
.changeset/describe-error-subpath.md Declares a patch release documenting the new public subpath and helper.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/core/src/describe-error.ts Outdated
Comment on lines +38 to +40
*/
export interface PersistedErrorSignal {
errorCode?: RunErrorCode;
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PersistedErrorSignal.errorCode is typed as RunErrorCode, but persisted run_failed.eventData.errorCode is currently just string | undefined (world schema allows any string for backwards compatibility). Making this field string | undefined (and coercing/validating to known RUN_ERROR_CODES internally) would avoid forcing downstream renderers to cast and prevents returning an invalid code under the RunErrorCode type.

Suggested change
*/
export interface PersistedErrorSignal {
errorCode?: RunErrorCode;
*
* Note that persisted `errorCode` is stored as a plain string for backward
* compatibility and must be validated/coerced against `RUN_ERROR_CODES`
* before being treated as a `RunErrorCode`.
*/
export interface PersistedErrorSignal {
errorCode?: string;

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e6b8e31PersistedErrorSignal.errorCode is now typed as string | undefined rather than RunErrorCode | undefined, matching the actual loose shape of the stored value. describeRunError runs it through a normalizeErrorCode helper that only trusts entries in RUN_ERROR_CODES and falls back to USER_ERROR otherwise.

Comment thread .changeset/describe-error-subpath.md Outdated
@@ -0,0 +1,10 @@
---
'@workflow/core': patch
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changeset frontmatter uses single quotes around the package name, but other changesets in this repo use double quotes. Please update the frontmatter to match existing changeset formatting so automated tooling remains consistent.

Suggested change
'@workflow/core': patch
"@workflow/core": patch

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e6b8e31 — single-quoted frontmatter switched to double-quoted.

Comment thread packages/core/src/describe-error.ts Outdated
Comment on lines +34 to +40
* event data: the classification `errorCode` plus an optional `errorName`
* (which maps to `Error#name` on the original throw). The UI does not
* have the original `Error` instance after hydration, so attribution has
* to be derived from these primitive fields.
*/
export interface PersistedErrorSignal {
errorCode?: RunErrorCode;
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docstring claims errorName is persisted in run_failed / step_failed event data, but the current event schemas/runtime only persist errorCode plus an error payload (no separate errorName). As a result, consumers reading hydrated events won’t be able to provide errorName to describeRunError unless the runtime/world schema is updated to persist it, or this helper is adjusted to derive the name from the persisted error payload instead.

Suggested change
* event data: the classification `errorCode` plus an optional `errorName`
* (which maps to `Error#name` on the original throw). The UI does not
* have the original `Error` instance after hydration, so attribution has
* to be derived from these primitive fields.
*/
export interface PersistedErrorSignal {
errorCode?: RunErrorCode;
* event data: the classification `errorCode` plus the serialized `error`
* payload emitted by the runtime. Some callers may also provide
* `errorName` as a compatibility field, but it is not a separately
* persisted primitive in the current event schema. The UI does not have
* the original `Error` instance after hydration, so attribution should be
* derived from `errorCode` plus the hydrated error payload when needed.
*/
export interface PersistedErrorSignal {
errorCode?: RunErrorCode;
error?: { name?: string } | null;

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in e6b8e31 — the docstring now notes that errorName is not universally persisted today, and explains that callers that have it can pass it in to sharpen the attribution. describeRunError still returns a sensible result from errorCode alone when errorName is absent.

@pranaygp
Copy link
Copy Markdown
Contributor Author

Stacked: #1840 — Phase 8 (final): WorkflowBuildError class + applications in @workflow/builders.

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>
@pranaygp pranaygp force-pushed the pranaygp/friendlier-errors-phase-7-observability branch from ff48e25 to e6b8e31 Compare April 24, 2026 01:23
@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.

2 participants