Skip to content

Presentation-only user vs SDK error attribution - #1837

Closed
pranaygp wants to merge 2 commits into
pranaygp/friendlier-errors-phase-4-serializationfrom
pranaygp/friendlier-errors-phase-5-attribution
Closed

Presentation-only user vs SDK error attribution#1837
pranaygp wants to merge 2 commits into
pranaygp/friendlier-errors-phase-4-serializationfrom
pranaygp/friendlier-errors-phase-5-attribution

Conversation

@pranaygp

@pranaygppranaygp commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Phase 5 of the friendlier-errors stack. Adds a presentation-only helper describeError() that computes user vs SDK attribution and class-aware hints from existing error classes and RUN_ERROR_CODES. No event data or persisted error classification is affected.

  • New packages/core/src/describe-error.ts returning { attribution, errorCode, hint? }
    • user for plain errors, SerializationError, and context-violation errors (NotInWorkflowContextError, etc.)
    • sdk for WorkflowRuntimeError (and subclasses like StepNotRegisteredError), replay timeouts, and max-delivery exhaustion
  • Terminal logs at step-failure, max-retries, run-failure, and fatal-setup sites now include errorAttribution metadata and a hint field for known error types.
  • 6 unit tests cover the attribution matrix.

Motivated by a real customer case where a vendor's 503 looked like an SDK failure.

Manual test plan

Using workbench/nextjs-turbopackpnpm dev and watch the terminal. Each test checks the structured metadata in the [workflow-sdk] log at step/run-failure time.

  • User error → attribution: 'user', no hint — throw plain Error('boom') from a step. Confirm the log metadata includes errorAttribution: 'user' and nohint field (plain user errors get no hint — stack is already useful).

  • SerializationError'user' + serialization hint — reuse the "unregistered class" test from Phase 4. Log metadata should include errorAttribution: 'user' and hint: 'A value passed across a workflow/step boundary could not be serialized. See the error message for the offending path and the Learn More link for details.'

  • Context-violation → 'user' + context hint — call createHook() from a route. Log metadata should include errorAttribution: 'user' and hint: 'A workflow-only or step-only API was called from the wrong context. The error message includes the exact API and how to move the call.'

  • WorkflowRuntimeError'sdk' + runtime hint — hard to trigger accidentally; throw one directly:

    import{WorkflowRuntimeError}from'@workflow/errors';asyncfunctions(){'use step';thrownewWorkflowRuntimeError('invariant broken');}

    Expect errorAttribution: 'sdk' and hint: 'This is an internal workflow SDK error, not a bug in your code. If it keeps happening, please report it with the stack trace and the runId.'

  • Replay timeout → 'sdk' + replay hint — set WORKFLOW_REPLAY_TIMEOUT_MS=50, run a non-trivial workflow. After retries exhaust, expect errorAttribution: 'sdk' and hint: 'The workflow replay took too long…'.

  • Max-delivery exhaustion → 'sdk' + max-delivery hint — write a step that always throws. After the queue's max-delivery budget exhausts, expect errorAttribution: 'sdk' and hint: 'The workflow queue exceeded its max-delivery budget…'.

  • Attribution applies at all four log sites — confirm the log contains errorAttribution + optional hint at:

    • step failure (non-terminal)
    • step max-retries (terminal)
    • run failure
    • fatal workflow setup

Unit tests

  • pnpm --filter @workflow/core exec vitest run src/describe-error.test.ts (6 tests)
  • Step-handler / runtime log-site changes don't affect classify-error.test.ts

📚 Friendlier errors stack

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

#PRPhaseSummary
1#1831Phase 1 + 2Ansi rendering primitives + context-violation errors
2#1832Phase 3Structured logger metadata; folds in #1812
3#1836Phase 4SerializationError at serialization / stream / encryption boundaries
4→ this PR (#1837)Phase 5Presentation-only user vs SDK attribution (describeError)
5#1838Phase 6Consistency pass on remaining bare throw new Error(...) sites
6#1839Phase 7 foundationData-driven describeRunError + public subpath
7#1840Phase 8WorkflowBuildError + applications in @workflow/builders
8#1849FollowupsDrop 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

CopilotAI review requested due to automatic review settings April 23, 2026 16:40
@vercel

vercelBot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
❌ 💻 Local Development10522861140
✅ 📦 Local Production10540861140
✅ 🐘 Local Postgres10540861140
✅ 📋 Other267018285
Total342722763705

❌ 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
AppPassedFailedSkipped
✅ astro-stable8906
✅ express-stable8906
✅ fastify-stable8906
✅ hono-stable8906
✅ nextjs-turbopack-canary76019
✅ nextjs-turbopack-stable9500
✅ nextjs-webpack-canary76019
✅ nextjs-webpack-stable9500
✅ nitro-stable8906
✅ nuxt-stable8906
✅ sveltekit-stable8906
❌ vite-stable8726
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable8906
✅ express-stable8906
✅ fastify-stable8906
✅ hono-stable8906
✅ nextjs-turbopack-canary76019
✅ nextjs-turbopack-stable9500
✅ nextjs-webpack-canary76019
✅ nextjs-webpack-stable9500
✅ nitro-stable8906
✅ nuxt-stable8906
✅ sveltekit-stable8906
✅ vite-stable8906
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable8906
✅ express-stable8906
✅ fastify-stable8906
✅ hono-stable8906
✅ nextjs-turbopack-canary76019
✅ nextjs-turbopack-stable9500
✅ nextjs-webpack-canary76019
✅ nextjs-webpack-stable9500
✅ nitro-stable8906
✅ nuxt-stable8906
✅ sveltekit-stable8906
✅ vite-stable8906
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable8906
✅ e2e-local-postgres-nest-stable8906
✅ e2e-local-prod-nest-stable8906

📋 View full workflow run


Some E2E test jobs failed:

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

Check the workflow run for details.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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 new presentation-only error description helper in @workflow/core to enrich runtime/step terminal logs with user-vs-SDK attribution and (when available) class-aware hints, without changing persisted error classification or event data.

Changes:

  • Introduce describeError() returning { attribution, errorCode, hint? } based on existing error classes.
  • Add errorAttribution (and optional hint) metadata to step-failure and run-failure log sites.
  • Add unit tests for the attribution matrix and a changeset entry.

Reviewed changes

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

Show a summary per file
FileDescription
packages/core/src/runtime/step-handler.tsEnriches step failure/max-retry logs with attribution + optional hint.
packages/core/src/runtime.tsEnriches fatal-setup and run-failure logs with attribution + optional hint.
packages/core/src/describe-error.tsNew helper implementing attribution/hint logic.
packages/core/src/describe-error.test.tsUnit tests validating attribution for key error types.
.changeset/friendlier-error-attribution.mdRelease note for new helper + logging metadata changes.

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

Comment threadpackages/core/src/describe-error.ts Outdated
Comment on lines +54 to +96
export function describeError(err: unknown): ErrorDescription {
const errorCode = classifyRunError(err);
const name = err instanceof Error ? err.name : undefined;

if (SerializationError.is(err)) {
return {
attribution: 'user',
errorCode,
hint: 'A value passed across a workflow/step boundary could not be serialized. See the error message for the offending path and the Learn More link for details.',
};
}

if (name && CONTEXT_ERROR_NAMES.has(name)) {
return {
attribution: 'user',
errorCode,
hint: 'A workflow-only or step-only API was called from the wrong context. The error message includes the exact API and how to move the call.',
};
}

if (err instanceof WorkflowRuntimeError) {
return {
attribution: 'sdk',
errorCode,
hint: 'This is an internal workflow SDK error, not a bug in your code. If it keeps happening, please report it with the stack trace and the runId.',
};
}

if (errorCode === RUN_ERROR_CODES.REPLAY_TIMEOUT) {
return {
attribution: 'sdk',
errorCode,
hint: 'The workflow replay took too long. This usually means the event log is unusually large or the workflow function is doing heavy synchronous work between step boundaries.',
};
}

if (errorCode === RUN_ERROR_CODES.MAX_DELIVERIES_EXCEEDED) {
return {
attribution: 'sdk',
errorCode,
hint: 'The workflow queue exceeded its max-delivery budget. This usually indicates a persistent runtime failure — check the most recent stack traces for the underlying cause.',
};
}

CopilotAIApr 23, 2026

Copy link

Choose a reason for hiding this comment

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

describeError() currently derives errorCode exclusively via classifyRunError(), which only returns USER_ERROR or RUNTIME_ERROR. That makes the REPLAY_TIMEOUT and MAX_DELIVERIES_EXCEEDED branches unreachable, so the SDK attribution/hints for those codes will never be produced. Consider changing the API to accept a precomputed RunErrorCode (or introducing a describeErrorCode() helper) and updating call sites that have these codes but no thrown error to use it; add tests covering those paths.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Fixed in 3dd68ccdescribeError now takes an optional errorCode parameter (see the MAX_DELIVERIES_EXCEEDED / REPLAY_TIMEOUT call sites in runtime.ts). Callers that already know the code pass it in; the internal branches match on that precomputed code, so they're no longer unreachable. Three new tests cover the paths.

Comment threadpackages/core/src/describe-error.ts Outdated
Comment on lines +32 to +71
const CONTEXT_ERROR_NAMES = new Set([
'NotInWorkflowContextError',
'NotInStepContextError',
'NotInWorkflowOrStepContextError',
'UnavailableInWorkflowContextError',
]);

/**
* Describe an error for user-facing presentation. Purely informational —
* does not change any persisted event data or error classification used by
* the runtime.
*
* The attribution here is more nuanced than `classifyRunError`:
*
* - `SerializationError` is technically raised by the SDK, but it almost
* always points at something the caller did (passed a non-serializable
* value, didn't register a class). We attribute it to the user.
* - Context-violation errors (`NotInWorkflowContextError`, etc.) likewise
* describe a user mistake.
* - `WorkflowRuntimeError` (and subclasses like `StepNotRegisteredError`)
* indicates an internal SDK invariant broke — surface that as `sdk`.
*/
export function describeError(err: unknown): ErrorDescription {
const errorCode = classifyRunError(err);
const name = err instanceof Error ? err.name : undefined;

if (SerializationError.is(err)) {
return {
attribution: 'user',
errorCode,
hint: 'A value passed across a workflow/step boundary could not be serialized. See the error message for the offending path and the Learn More link for details.',
};
}

if (name && CONTEXT_ERROR_NAMES.has(name)) {
return {
attribution: 'user',
errorCode,
hint: 'A workflow-only or step-only API was called from the wrong context. The error message includes the exact API and how to move the call.',
};

CopilotAIApr 23, 2026

Copy link

Choose a reason for hiding this comment

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

describeError() detects context-violation errors by comparing err.name against a string set. Since the concrete context error classes live in ./context-errors.ts, using instanceof checks (or a shared type guard) would be more robust and would fail at compile-time if those classes are renamed, instead of silently losing attribution/hints.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Fixed in 3dd68ccdescribeError now detects context-violation errors with instanceof against the concrete classes (NotInWorkflowContextError et al.) rather than a string Set of names, so renames can't silently break the classification.

Comment on lines 578 to 595
const errorCode = classifyRunError(err);
const description = describeError(err);
const framing =
description.attribution === 'sdk'
? `Workflow "${workflowName}" failed due to an SDK runtime error`
: `Workflow "${workflowName}" threw`;

// Use the stack as the primary message so it shows up
// in flattened logs without structured metadata.
runLogger.error(
errorStack || 'Unknown error encountered in workflow',
`${framing}\n${errorStack || 'Unknown error encountered in workflow'}`,
{
errorCode,
errorAttribution: description.attribution,
errorName,
errorMessage,
...(description.hint ? { hint: description.hint } : {}),
}

CopilotAIApr 23, 2026

Copy link

Choose a reason for hiding this comment

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

In this failure path errorCode is computed via classifyRunError(err) and describeError(err) recomputes it internally. Consider passing the already-computed errorCode into describeError (or returning attribution/hint separately) to avoid duplicated work and guarantee the logged errorCode and the description stay in sync if the classification logic evolves.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Addressed in 3dd68cc — the call site in runtime.ts now passes the precomputed errorCode into describeError(err, errorCode), so classifyRunError is only called once.

@@ -0,0 +1,11 @@
---
'@workflow/core': patch

CopilotAIApr 23, 2026

Copy link

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 consistently use double quotes (YAML string). For consistency (and to match existing tooling expectations), switch this to "@workflow/core": patch.

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

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Fixed in 3dd68cc — single-quoted frontmatter switched to double-quoted.

@pranaygp

Copy link
Copy Markdown
ContributorAuthor

Stacked: #1838 — Phase 6, cosmetic consistency pass on remaining bare throws

pranaygpand others added 2 commits April 23, 2026 18:15
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.
@pranaygp
pranaygpforce-pushed the pranaygp/friendlier-errors-phase-5-attribution branch from eb171bb to 3dd68ccCompareApril 24, 2026 01:18
@changeset-bot

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 3dd68cc

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

This PR includes changesets to release 17 packages
NameType
@workflow/corePatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch
@workflow/aiPatch

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

@pranaygp

Copy link
Copy Markdown
ContributorAuthor

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

Sign up for freeto 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

@pranaygp
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Presentation-only user vs SDK error attribution by pranaygp · Pull Request #1837 · vercel/workflow · GitHub
Skip to content

Presentation-only user vs SDK error attribution - #1837

Closed
pranaygp wants to merge 2 commits into
pranaygp/friendlier-errors-phase-4-serializationfrom
pranaygp/friendlier-errors-phase-5-attribution
Closed

Presentation-only user vs SDK error attribution#1837
pranaygp wants to merge 2 commits into
pranaygp/friendlier-errors-phase-4-serializationfrom
pranaygp/friendlier-errors-phase-5-attribution

Conversation

@pranaygp

@pranaygppranaygp commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Phase 5 of the friendlier-errors stack. Adds a presentation-only helper describeError() that computes user vs SDK attribution and class-aware hints from existing error classes and RUN_ERROR_CODES. No event data or persisted error classification is affected.

  • New packages/core/src/describe-error.ts returning { attribution, errorCode, hint? }
    • user for plain errors, SerializationError, and context-violation errors (NotInWorkflowContextError, etc.)
    • sdk for WorkflowRuntimeError (and subclasses like StepNotRegisteredError), replay timeouts, and max-delivery exhaustion
  • Terminal logs at step-failure, max-retries, run-failure, and fatal-setup sites now include errorAttribution metadata and a hint field for known error types.
  • 6 unit tests cover the attribution matrix.

Motivated by a real customer case where a vendor's 503 looked like an SDK failure.

Manual test plan

Using workbench/nextjs-turbopackpnpm dev and watch the terminal. Each test checks the structured metadata in the [workflow-sdk] log at step/run-failure time.

  • User error → attribution: 'user', no hint — throw plain Error('boom') from a step. Confirm the log metadata includes errorAttribution: 'user' and nohint field (plain user errors get no hint — stack is already useful).

  • SerializationError'user' + serialization hint — reuse the "unregistered class" test from Phase 4. Log metadata should include errorAttribution: 'user' and hint: 'A value passed across a workflow/step boundary could not be serialized. See the error message for the offending path and the Learn More link for details.'

  • Context-violation → 'user' + context hint — call createHook() from a route. Log metadata should include errorAttribution: 'user' and hint: 'A workflow-only or step-only API was called from the wrong context. The error message includes the exact API and how to move the call.'

  • WorkflowRuntimeError'sdk' + runtime hint — hard to trigger accidentally; throw one directly:

    import{WorkflowRuntimeError}from'@workflow/errors';asyncfunctions(){'use step';thrownewWorkflowRuntimeError('invariant broken');}

    Expect errorAttribution: 'sdk' and hint: 'This is an internal workflow SDK error, not a bug in your code. If it keeps happening, please report it with the stack trace and the runId.'

  • Replay timeout → 'sdk' + replay hint — set WORKFLOW_REPLAY_TIMEOUT_MS=50, run a non-trivial workflow. After retries exhaust, expect errorAttribution: 'sdk' and hint: 'The workflow replay took too long…'.

  • Max-delivery exhaustion → 'sdk' + max-delivery hint — write a step that always throws. After the queue's max-delivery budget exhausts, expect errorAttribution: 'sdk' and hint: 'The workflow queue exceeded its max-delivery budget…'.

  • Attribution applies at all four log sites — confirm the log contains errorAttribution + optional hint at:

    • step failure (non-terminal)
    • step max-retries (terminal)
    • run failure
    • fatal workflow setup

Unit tests

  • pnpm --filter @workflow/core exec vitest run src/describe-error.test.ts (6 tests)
  • Step-handler / runtime log-site changes don't affect classify-error.test.ts

📚 Friendlier errors stack

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

#PRPhaseSummary
1#1831Phase 1 + 2Ansi rendering primitives + context-violation errors
2#1832Phase 3Structured logger metadata; folds in #1812
3#1836Phase 4SerializationError at serialization / stream / encryption boundaries
4→ this PR (#1837)Phase 5Presentation-only user vs SDK attribution (describeError)
5#1838Phase 6Consistency pass on remaining bare throw new Error(...) sites
6#1839Phase 7 foundationData-driven describeRunError + public subpath
7#1840Phase 8WorkflowBuildError + applications in @workflow/builders
8#1849FollowupsDrop 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

CopilotAI review requested due to automatic review settings April 23, 2026 16:40
@vercel

vercelBot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
❌ 💻 Local Development10522861140
✅ 📦 Local Production10540861140
✅ 🐘 Local Postgres10540861140
✅ 📋 Other267018285
Total342722763705

❌ 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
AppPassedFailedSkipped
✅ astro-stable8906
✅ express-stable8906
✅ fastify-stable8906
✅ hono-stable8906
✅ nextjs-turbopack-canary76019
✅ nextjs-turbopack-stable9500
✅ nextjs-webpack-canary76019
✅ nextjs-webpack-stable9500
✅ nitro-stable8906
✅ nuxt-stable8906
✅ sveltekit-stable8906
❌ vite-stable8726
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable8906
✅ express-stable8906
✅ fastify-stable8906
✅ hono-stable8906
✅ nextjs-turbopack-canary76019
✅ nextjs-turbopack-stable9500
✅ nextjs-webpack-canary76019
✅ nextjs-webpack-stable9500
✅ nitro-stable8906
✅ nuxt-stable8906
✅ sveltekit-stable8906
✅ vite-stable8906
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable8906
✅ express-stable8906
✅ fastify-stable8906
✅ hono-stable8906
✅ nextjs-turbopack-canary76019
✅ nextjs-turbopack-stable9500
✅ nextjs-webpack-canary76019
✅ nextjs-webpack-stable9500
✅ nitro-stable8906
✅ nuxt-stable8906
✅ sveltekit-stable8906
✅ vite-stable8906
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable8906
✅ e2e-local-postgres-nest-stable8906
✅ e2e-local-prod-nest-stable8906

📋 View full workflow run


Some E2E test jobs failed:

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

Check the workflow run for details.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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 new presentation-only error description helper in @workflow/core to enrich runtime/step terminal logs with user-vs-SDK attribution and (when available) class-aware hints, without changing persisted error classification or event data.

Changes:

  • Introduce describeError() returning { attribution, errorCode, hint? } based on existing error classes.
  • Add errorAttribution (and optional hint) metadata to step-failure and run-failure log sites.
  • Add unit tests for the attribution matrix and a changeset entry.

Reviewed changes

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

Show a summary per file
FileDescription
packages/core/src/runtime/step-handler.tsEnriches step failure/max-retry logs with attribution + optional hint.
packages/core/src/runtime.tsEnriches fatal-setup and run-failure logs with attribution + optional hint.
packages/core/src/describe-error.tsNew helper implementing attribution/hint logic.
packages/core/src/describe-error.test.tsUnit tests validating attribution for key error types.
.changeset/friendlier-error-attribution.mdRelease note for new helper + logging metadata changes.

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

Comment threadpackages/core/src/describe-error.ts Outdated
Comment on lines +54 to +96
export function describeError(err: unknown): ErrorDescription {
const errorCode = classifyRunError(err);
const name = err instanceof Error ? err.name : undefined;

if (SerializationError.is(err)) {
return {
attribution: 'user',
errorCode,
hint: 'A value passed across a workflow/step boundary could not be serialized. See the error message for the offending path and the Learn More link for details.',
};
}

if (name && CONTEXT_ERROR_NAMES.has(name)) {
return {
attribution: 'user',
errorCode,
hint: 'A workflow-only or step-only API was called from the wrong context. The error message includes the exact API and how to move the call.',
};
}

if (err instanceof WorkflowRuntimeError) {
return {
attribution: 'sdk',
errorCode,
hint: 'This is an internal workflow SDK error, not a bug in your code. If it keeps happening, please report it with the stack trace and the runId.',
};
}

if (errorCode === RUN_ERROR_CODES.REPLAY_TIMEOUT) {
return {
attribution: 'sdk',
errorCode,
hint: 'The workflow replay took too long. This usually means the event log is unusually large or the workflow function is doing heavy synchronous work between step boundaries.',
};
}

if (errorCode === RUN_ERROR_CODES.MAX_DELIVERIES_EXCEEDED) {
return {
attribution: 'sdk',
errorCode,
hint: 'The workflow queue exceeded its max-delivery budget. This usually indicates a persistent runtime failure — check the most recent stack traces for the underlying cause.',
};
}

CopilotAIApr 23, 2026

Copy link

Choose a reason for hiding this comment

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

describeError() currently derives errorCode exclusively via classifyRunError(), which only returns USER_ERROR or RUNTIME_ERROR. That makes the REPLAY_TIMEOUT and MAX_DELIVERIES_EXCEEDED branches unreachable, so the SDK attribution/hints for those codes will never be produced. Consider changing the API to accept a precomputed RunErrorCode (or introducing a describeErrorCode() helper) and updating call sites that have these codes but no thrown error to use it; add tests covering those paths.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Fixed in 3dd68ccdescribeError now takes an optional errorCode parameter (see the MAX_DELIVERIES_EXCEEDED / REPLAY_TIMEOUT call sites in runtime.ts). Callers that already know the code pass it in; the internal branches match on that precomputed code, so they're no longer unreachable. Three new tests cover the paths.

Comment threadpackages/core/src/describe-error.ts Outdated
Comment on lines +32 to +71
const CONTEXT_ERROR_NAMES = new Set([
'NotInWorkflowContextError',
'NotInStepContextError',
'NotInWorkflowOrStepContextError',
'UnavailableInWorkflowContextError',
]);

/**
* Describe an error for user-facing presentation. Purely informational —
* does not change any persisted event data or error classification used by
* the runtime.
*
* The attribution here is more nuanced than `classifyRunError`:
*
* - `SerializationError` is technically raised by the SDK, but it almost
* always points at something the caller did (passed a non-serializable
* value, didn't register a class). We attribute it to the user.
* - Context-violation errors (`NotInWorkflowContextError`, etc.) likewise
* describe a user mistake.
* - `WorkflowRuntimeError` (and subclasses like `StepNotRegisteredError`)
* indicates an internal SDK invariant broke — surface that as `sdk`.
*/
export function describeError(err: unknown): ErrorDescription {
const errorCode = classifyRunError(err);
const name = err instanceof Error ? err.name : undefined;

if (SerializationError.is(err)) {
return {
attribution: 'user',
errorCode,
hint: 'A value passed across a workflow/step boundary could not be serialized. See the error message for the offending path and the Learn More link for details.',
};
}

if (name && CONTEXT_ERROR_NAMES.has(name)) {
return {
attribution: 'user',
errorCode,
hint: 'A workflow-only or step-only API was called from the wrong context. The error message includes the exact API and how to move the call.',
};

CopilotAIApr 23, 2026

Copy link

Choose a reason for hiding this comment

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

describeError() detects context-violation errors by comparing err.name against a string set. Since the concrete context error classes live in ./context-errors.ts, using instanceof checks (or a shared type guard) would be more robust and would fail at compile-time if those classes are renamed, instead of silently losing attribution/hints.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Fixed in 3dd68ccdescribeError now detects context-violation errors with instanceof against the concrete classes (NotInWorkflowContextError et al.) rather than a string Set of names, so renames can't silently break the classification.

Comment on lines 578 to 595
const errorCode = classifyRunError(err);
const description = describeError(err);
const framing =
description.attribution === 'sdk'
? `Workflow "${workflowName}" failed due to an SDK runtime error`
: `Workflow "${workflowName}" threw`;

// Use the stack as the primary message so it shows up
// in flattened logs without structured metadata.
runLogger.error(
errorStack || 'Unknown error encountered in workflow',
`${framing}\n${errorStack || 'Unknown error encountered in workflow'}`,
{
errorCode,
errorAttribution: description.attribution,
errorName,
errorMessage,
...(description.hint ? { hint: description.hint } : {}),
}

CopilotAIApr 23, 2026

Copy link

Choose a reason for hiding this comment

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

In this failure path errorCode is computed via classifyRunError(err) and describeError(err) recomputes it internally. Consider passing the already-computed errorCode into describeError (or returning attribution/hint separately) to avoid duplicated work and guarantee the logged errorCode and the description stay in sync if the classification logic evolves.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Addressed in 3dd68cc — the call site in runtime.ts now passes the precomputed errorCode into describeError(err, errorCode), so classifyRunError is only called once.

@@ -0,0 +1,11 @@
---
'@workflow/core': patch

CopilotAIApr 23, 2026

Copy link

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 consistently use double quotes (YAML string). For consistency (and to match existing tooling expectations), switch this to "@workflow/core": patch.

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

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Fixed in 3dd68cc — single-quoted frontmatter switched to double-quoted.

@pranaygp

Copy link
Copy Markdown
ContributorAuthor

Stacked: #1838 — Phase 6, cosmetic consistency pass on remaining bare throws

pranaygpand others added 2 commits April 23, 2026 18:15
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.
@pranaygp
pranaygpforce-pushed the pranaygp/friendlier-errors-phase-5-attribution branch from eb171bb to 3dd68ccCompareApril 24, 2026 01:18
@changeset-bot

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 3dd68cc

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

This PR includes changesets to release 17 packages
NameType
@workflow/corePatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch
@workflow/aiPatch

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

@pranaygp

Copy link
Copy Markdown
ContributorAuthor

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

Sign up for freeto 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

@pranaygp
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Presentation-only user vs SDK error attribution by pranaygp · Pull Request #1837 · vercel/workflow · GitHub
Skip to content

Presentation-only user vs SDK error attribution - #1837

Closed
pranaygp wants to merge 2 commits into
pranaygp/friendlier-errors-phase-4-serializationfrom
pranaygp/friendlier-errors-phase-5-attribution
Closed

Presentation-only user vs SDK error attribution#1837
pranaygp wants to merge 2 commits into
pranaygp/friendlier-errors-phase-4-serializationfrom
pranaygp/friendlier-errors-phase-5-attribution

Conversation

@pranaygp

@pranaygppranaygp commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Phase 5 of the friendlier-errors stack. Adds a presentation-only helper describeError() that computes user vs SDK attribution and class-aware hints from existing error classes and RUN_ERROR_CODES. No event data or persisted error classification is affected.

  • New packages/core/src/describe-error.ts returning { attribution, errorCode, hint? }
    • user for plain errors, SerializationError, and context-violation errors (NotInWorkflowContextError, etc.)
    • sdk for WorkflowRuntimeError (and subclasses like StepNotRegisteredError), replay timeouts, and max-delivery exhaustion
  • Terminal logs at step-failure, max-retries, run-failure, and fatal-setup sites now include errorAttribution metadata and a hint field for known error types.
  • 6 unit tests cover the attribution matrix.

Motivated by a real customer case where a vendor's 503 looked like an SDK failure.

Manual test plan

Using workbench/nextjs-turbopackpnpm dev and watch the terminal. Each test checks the structured metadata in the [workflow-sdk] log at step/run-failure time.

  • User error → attribution: 'user', no hint — throw plain Error('boom') from a step. Confirm the log metadata includes errorAttribution: 'user' and nohint field (plain user errors get no hint — stack is already useful).

  • SerializationError'user' + serialization hint — reuse the "unregistered class" test from Phase 4. Log metadata should include errorAttribution: 'user' and hint: 'A value passed across a workflow/step boundary could not be serialized. See the error message for the offending path and the Learn More link for details.'

  • Context-violation → 'user' + context hint — call createHook() from a route. Log metadata should include errorAttribution: 'user' and hint: 'A workflow-only or step-only API was called from the wrong context. The error message includes the exact API and how to move the call.'

  • WorkflowRuntimeError'sdk' + runtime hint — hard to trigger accidentally; throw one directly:

    import{WorkflowRuntimeError}from'@workflow/errors';asyncfunctions(){'use step';thrownewWorkflowRuntimeError('invariant broken');}

    Expect errorAttribution: 'sdk' and hint: 'This is an internal workflow SDK error, not a bug in your code. If it keeps happening, please report it with the stack trace and the runId.'

  • Replay timeout → 'sdk' + replay hint — set WORKFLOW_REPLAY_TIMEOUT_MS=50, run a non-trivial workflow. After retries exhaust, expect errorAttribution: 'sdk' and hint: 'The workflow replay took too long…'.

  • Max-delivery exhaustion → 'sdk' + max-delivery hint — write a step that always throws. After the queue's max-delivery budget exhausts, expect errorAttribution: 'sdk' and hint: 'The workflow queue exceeded its max-delivery budget…'.

  • Attribution applies at all four log sites — confirm the log contains errorAttribution + optional hint at:

    • step failure (non-terminal)
    • step max-retries (terminal)
    • run failure
    • fatal workflow setup

Unit tests

  • pnpm --filter @workflow/core exec vitest run src/describe-error.test.ts (6 tests)
  • Step-handler / runtime log-site changes don't affect classify-error.test.ts

📚 Friendlier errors stack

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

#PRPhaseSummary
1#1831Phase 1 + 2Ansi rendering primitives + context-violation errors
2#1832Phase 3Structured logger metadata; folds in #1812
3#1836Phase 4SerializationError at serialization / stream / encryption boundaries
4→ this PR (#1837)Phase 5Presentation-only user vs SDK attribution (describeError)
5#1838Phase 6Consistency pass on remaining bare throw new Error(...) sites
6#1839Phase 7 foundationData-driven describeRunError + public subpath
7#1840Phase 8WorkflowBuildError + applications in @workflow/builders
8#1849FollowupsDrop 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

CopilotAI review requested due to automatic review settings April 23, 2026 16:40
@vercel

vercelBot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
❌ 💻 Local Development10522861140
✅ 📦 Local Production10540861140
✅ 🐘 Local Postgres10540861140
✅ 📋 Other267018285
Total342722763705

❌ 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
AppPassedFailedSkipped
✅ astro-stable8906
✅ express-stable8906
✅ fastify-stable8906
✅ hono-stable8906
✅ nextjs-turbopack-canary76019
✅ nextjs-turbopack-stable9500
✅ nextjs-webpack-canary76019
✅ nextjs-webpack-stable9500
✅ nitro-stable8906
✅ nuxt-stable8906
✅ sveltekit-stable8906
❌ vite-stable8726
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable8906
✅ express-stable8906
✅ fastify-stable8906
✅ hono-stable8906
✅ nextjs-turbopack-canary76019
✅ nextjs-turbopack-stable9500
✅ nextjs-webpack-canary76019
✅ nextjs-webpack-stable9500
✅ nitro-stable8906
✅ nuxt-stable8906
✅ sveltekit-stable8906
✅ vite-stable8906
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable8906
✅ express-stable8906
✅ fastify-stable8906
✅ hono-stable8906
✅ nextjs-turbopack-canary76019
✅ nextjs-turbopack-stable9500
✅ nextjs-webpack-canary76019
✅ nextjs-webpack-stable9500
✅ nitro-stable8906
✅ nuxt-stable8906
✅ sveltekit-stable8906
✅ vite-stable8906
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable8906
✅ e2e-local-postgres-nest-stable8906
✅ e2e-local-prod-nest-stable8906

📋 View full workflow run


Some E2E test jobs failed:

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

Check the workflow run for details.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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 new presentation-only error description helper in @workflow/core to enrich runtime/step terminal logs with user-vs-SDK attribution and (when available) class-aware hints, without changing persisted error classification or event data.

Changes:

  • Introduce describeError() returning { attribution, errorCode, hint? } based on existing error classes.
  • Add errorAttribution (and optional hint) metadata to step-failure and run-failure log sites.
  • Add unit tests for the attribution matrix and a changeset entry.

Reviewed changes

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

Show a summary per file
FileDescription
packages/core/src/runtime/step-handler.tsEnriches step failure/max-retry logs with attribution + optional hint.
packages/core/src/runtime.tsEnriches fatal-setup and run-failure logs with attribution + optional hint.
packages/core/src/describe-error.tsNew helper implementing attribution/hint logic.
packages/core/src/describe-error.test.tsUnit tests validating attribution for key error types.
.changeset/friendlier-error-attribution.mdRelease note for new helper + logging metadata changes.

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

Comment threadpackages/core/src/describe-error.ts Outdated
Comment on lines +54 to +96
export function describeError(err: unknown): ErrorDescription {
const errorCode = classifyRunError(err);
const name = err instanceof Error ? err.name : undefined;

if (SerializationError.is(err)) {
return {
attribution: 'user',
errorCode,
hint: 'A value passed across a workflow/step boundary could not be serialized. See the error message for the offending path and the Learn More link for details.',
};
}

if (name && CONTEXT_ERROR_NAMES.has(name)) {
return {
attribution: 'user',
errorCode,
hint: 'A workflow-only or step-only API was called from the wrong context. The error message includes the exact API and how to move the call.',
};
}

if (err instanceof WorkflowRuntimeError) {
return {
attribution: 'sdk',
errorCode,
hint: 'This is an internal workflow SDK error, not a bug in your code. If it keeps happening, please report it with the stack trace and the runId.',
};
}

if (errorCode === RUN_ERROR_CODES.REPLAY_TIMEOUT) {
return {
attribution: 'sdk',
errorCode,
hint: 'The workflow replay took too long. This usually means the event log is unusually large or the workflow function is doing heavy synchronous work between step boundaries.',
};
}

if (errorCode === RUN_ERROR_CODES.MAX_DELIVERIES_EXCEEDED) {
return {
attribution: 'sdk',
errorCode,
hint: 'The workflow queue exceeded its max-delivery budget. This usually indicates a persistent runtime failure — check the most recent stack traces for the underlying cause.',
};
}

CopilotAIApr 23, 2026

Copy link

Choose a reason for hiding this comment

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

describeError() currently derives errorCode exclusively via classifyRunError(), which only returns USER_ERROR or RUNTIME_ERROR. That makes the REPLAY_TIMEOUT and MAX_DELIVERIES_EXCEEDED branches unreachable, so the SDK attribution/hints for those codes will never be produced. Consider changing the API to accept a precomputed RunErrorCode (or introducing a describeErrorCode() helper) and updating call sites that have these codes but no thrown error to use it; add tests covering those paths.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Fixed in 3dd68ccdescribeError now takes an optional errorCode parameter (see the MAX_DELIVERIES_EXCEEDED / REPLAY_TIMEOUT call sites in runtime.ts). Callers that already know the code pass it in; the internal branches match on that precomputed code, so they're no longer unreachable. Three new tests cover the paths.

Comment threadpackages/core/src/describe-error.ts Outdated
Comment on lines +32 to +71
const CONTEXT_ERROR_NAMES = new Set([
'NotInWorkflowContextError',
'NotInStepContextError',
'NotInWorkflowOrStepContextError',
'UnavailableInWorkflowContextError',
]);

/**
* Describe an error for user-facing presentation. Purely informational —
* does not change any persisted event data or error classification used by
* the runtime.
*
* The attribution here is more nuanced than `classifyRunError`:
*
* - `SerializationError` is technically raised by the SDK, but it almost
* always points at something the caller did (passed a non-serializable
* value, didn't register a class). We attribute it to the user.
* - Context-violation errors (`NotInWorkflowContextError`, etc.) likewise
* describe a user mistake.
* - `WorkflowRuntimeError` (and subclasses like `StepNotRegisteredError`)
* indicates an internal SDK invariant broke — surface that as `sdk`.
*/
export function describeError(err: unknown): ErrorDescription {
const errorCode = classifyRunError(err);
const name = err instanceof Error ? err.name : undefined;

if (SerializationError.is(err)) {
return {
attribution: 'user',
errorCode,
hint: 'A value passed across a workflow/step boundary could not be serialized. See the error message for the offending path and the Learn More link for details.',
};
}

if (name && CONTEXT_ERROR_NAMES.has(name)) {
return {
attribution: 'user',
errorCode,
hint: 'A workflow-only or step-only API was called from the wrong context. The error message includes the exact API and how to move the call.',
};

CopilotAIApr 23, 2026

Copy link

Choose a reason for hiding this comment

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

describeError() detects context-violation errors by comparing err.name against a string set. Since the concrete context error classes live in ./context-errors.ts, using instanceof checks (or a shared type guard) would be more robust and would fail at compile-time if those classes are renamed, instead of silently losing attribution/hints.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Fixed in 3dd68ccdescribeError now detects context-violation errors with instanceof against the concrete classes (NotInWorkflowContextError et al.) rather than a string Set of names, so renames can't silently break the classification.

Comment on lines 578 to 595
const errorCode = classifyRunError(err);
const description = describeError(err);
const framing =
description.attribution === 'sdk'
? `Workflow "${workflowName}" failed due to an SDK runtime error`
: `Workflow "${workflowName}" threw`;

// Use the stack as the primary message so it shows up
// in flattened logs without structured metadata.
runLogger.error(
errorStack || 'Unknown error encountered in workflow',
`${framing}\n${errorStack || 'Unknown error encountered in workflow'}`,
{
errorCode,
errorAttribution: description.attribution,
errorName,
errorMessage,
...(description.hint ? { hint: description.hint } : {}),
}

CopilotAIApr 23, 2026

Copy link

Choose a reason for hiding this comment

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

In this failure path errorCode is computed via classifyRunError(err) and describeError(err) recomputes it internally. Consider passing the already-computed errorCode into describeError (or returning attribution/hint separately) to avoid duplicated work and guarantee the logged errorCode and the description stay in sync if the classification logic evolves.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Addressed in 3dd68cc — the call site in runtime.ts now passes the precomputed errorCode into describeError(err, errorCode), so classifyRunError is only called once.

@@ -0,0 +1,11 @@
---
'@workflow/core': patch

CopilotAIApr 23, 2026

Copy link

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 consistently use double quotes (YAML string). For consistency (and to match existing tooling expectations), switch this to "@workflow/core": patch.

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

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Fixed in 3dd68cc — single-quoted frontmatter switched to double-quoted.

@pranaygp

Copy link
Copy Markdown
ContributorAuthor

Stacked: #1838 — Phase 6, cosmetic consistency pass on remaining bare throws

pranaygpand others added 2 commits April 23, 2026 18:15
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.
@pranaygp
pranaygpforce-pushed the pranaygp/friendlier-errors-phase-5-attribution branch from eb171bb to 3dd68ccCompareApril 24, 2026 01:18
@changeset-bot

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 3dd68cc

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

This PR includes changesets to release 17 packages
NameType
@workflow/corePatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch
@workflow/aiPatch

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

@pranaygp

Copy link
Copy Markdown
ContributorAuthor

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

Sign up for freeto 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

@pranaygp
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Presentation-only user vs SDK error attribution by pranaygp · Pull Request #1837 · vercel/workflow · GitHub
Skip to content

Presentation-only user vs SDK error attribution - #1837

Closed
pranaygp wants to merge 2 commits into
pranaygp/friendlier-errors-phase-4-serializationfrom
pranaygp/friendlier-errors-phase-5-attribution
Closed

Presentation-only user vs SDK error attribution#1837
pranaygp wants to merge 2 commits into
pranaygp/friendlier-errors-phase-4-serializationfrom
pranaygp/friendlier-errors-phase-5-attribution

Conversation

@pranaygp

@pranaygppranaygp commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Phase 5 of the friendlier-errors stack. Adds a presentation-only helper describeError() that computes user vs SDK attribution and class-aware hints from existing error classes and RUN_ERROR_CODES. No event data or persisted error classification is affected.

  • New packages/core/src/describe-error.ts returning { attribution, errorCode, hint? }
    • user for plain errors, SerializationError, and context-violation errors (NotInWorkflowContextError, etc.)
    • sdk for WorkflowRuntimeError (and subclasses like StepNotRegisteredError), replay timeouts, and max-delivery exhaustion
  • Terminal logs at step-failure, max-retries, run-failure, and fatal-setup sites now include errorAttribution metadata and a hint field for known error types.
  • 6 unit tests cover the attribution matrix.

Motivated by a real customer case where a vendor's 503 looked like an SDK failure.

Manual test plan

Using workbench/nextjs-turbopackpnpm dev and watch the terminal. Each test checks the structured metadata in the [workflow-sdk] log at step/run-failure time.

  • User error → attribution: 'user', no hint — throw plain Error('boom') from a step. Confirm the log metadata includes errorAttribution: 'user' and nohint field (plain user errors get no hint — stack is already useful).

  • SerializationError'user' + serialization hint — reuse the "unregistered class" test from Phase 4. Log metadata should include errorAttribution: 'user' and hint: 'A value passed across a workflow/step boundary could not be serialized. See the error message for the offending path and the Learn More link for details.'

  • Context-violation → 'user' + context hint — call createHook() from a route. Log metadata should include errorAttribution: 'user' and hint: 'A workflow-only or step-only API was called from the wrong context. The error message includes the exact API and how to move the call.'

  • WorkflowRuntimeError'sdk' + runtime hint — hard to trigger accidentally; throw one directly:

    import{WorkflowRuntimeError}from'@workflow/errors';asyncfunctions(){'use step';thrownewWorkflowRuntimeError('invariant broken');}

    Expect errorAttribution: 'sdk' and hint: 'This is an internal workflow SDK error, not a bug in your code. If it keeps happening, please report it with the stack trace and the runId.'

  • Replay timeout → 'sdk' + replay hint — set WORKFLOW_REPLAY_TIMEOUT_MS=50, run a non-trivial workflow. After retries exhaust, expect errorAttribution: 'sdk' and hint: 'The workflow replay took too long…'.

  • Max-delivery exhaustion → 'sdk' + max-delivery hint — write a step that always throws. After the queue's max-delivery budget exhausts, expect errorAttribution: 'sdk' and hint: 'The workflow queue exceeded its max-delivery budget…'.

  • Attribution applies at all four log sites — confirm the log contains errorAttribution + optional hint at:

    • step failure (non-terminal)
    • step max-retries (terminal)
    • run failure
    • fatal workflow setup

Unit tests

  • pnpm --filter @workflow/core exec vitest run src/describe-error.test.ts (6 tests)
  • Step-handler / runtime log-site changes don't affect classify-error.test.ts

📚 Friendlier errors stack

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

#PRPhaseSummary
1#1831Phase 1 + 2Ansi rendering primitives + context-violation errors
2#1832Phase 3Structured logger metadata; folds in #1812
3#1836Phase 4SerializationError at serialization / stream / encryption boundaries
4→ this PR (#1837)Phase 5Presentation-only user vs SDK attribution (describeError)
5#1838Phase 6Consistency pass on remaining bare throw new Error(...) sites
6#1839Phase 7 foundationData-driven describeRunError + public subpath
7#1840Phase 8WorkflowBuildError + applications in @workflow/builders
8#1849FollowupsDrop 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

CopilotAI review requested due to automatic review settings April 23, 2026 16:40
@vercel

vercelBot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
❌ 💻 Local Development10522861140
✅ 📦 Local Production10540861140
✅ 🐘 Local Postgres10540861140
✅ 📋 Other267018285
Total342722763705

❌ 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
AppPassedFailedSkipped
✅ astro-stable8906
✅ express-stable8906
✅ fastify-stable8906
✅ hono-stable8906
✅ nextjs-turbopack-canary76019
✅ nextjs-turbopack-stable9500
✅ nextjs-webpack-canary76019
✅ nextjs-webpack-stable9500
✅ nitro-stable8906
✅ nuxt-stable8906
✅ sveltekit-stable8906
❌ vite-stable8726
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable8906
✅ express-stable8906
✅ fastify-stable8906
✅ hono-stable8906
✅ nextjs-turbopack-canary76019
✅ nextjs-turbopack-stable9500
✅ nextjs-webpack-canary76019
✅ nextjs-webpack-stable9500
✅ nitro-stable8906
✅ nuxt-stable8906
✅ sveltekit-stable8906
✅ vite-stable8906
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable8906
✅ express-stable8906
✅ fastify-stable8906
✅ hono-stable8906
✅ nextjs-turbopack-canary76019
✅ nextjs-turbopack-stable9500
✅ nextjs-webpack-canary76019
✅ nextjs-webpack-stable9500
✅ nitro-stable8906
✅ nuxt-stable8906
✅ sveltekit-stable8906
✅ vite-stable8906
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable8906
✅ e2e-local-postgres-nest-stable8906
✅ e2e-local-prod-nest-stable8906

📋 View full workflow run


Some E2E test jobs failed:

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

Check the workflow run for details.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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 new presentation-only error description helper in @workflow/core to enrich runtime/step terminal logs with user-vs-SDK attribution and (when available) class-aware hints, without changing persisted error classification or event data.

Changes:

  • Introduce describeError() returning { attribution, errorCode, hint? } based on existing error classes.
  • Add errorAttribution (and optional hint) metadata to step-failure and run-failure log sites.
  • Add unit tests for the attribution matrix and a changeset entry.

Reviewed changes

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

Show a summary per file
FileDescription
packages/core/src/runtime/step-handler.tsEnriches step failure/max-retry logs with attribution + optional hint.
packages/core/src/runtime.tsEnriches fatal-setup and run-failure logs with attribution + optional hint.
packages/core/src/describe-error.tsNew helper implementing attribution/hint logic.
packages/core/src/describe-error.test.tsUnit tests validating attribution for key error types.
.changeset/friendlier-error-attribution.mdRelease note for new helper + logging metadata changes.

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

Comment threadpackages/core/src/describe-error.ts Outdated
Comment on lines +54 to +96
export function describeError(err: unknown): ErrorDescription {
const errorCode = classifyRunError(err);
const name = err instanceof Error ? err.name : undefined;

if (SerializationError.is(err)) {
return {
attribution: 'user',
errorCode,
hint: 'A value passed across a workflow/step boundary could not be serialized. See the error message for the offending path and the Learn More link for details.',
};
}

if (name && CONTEXT_ERROR_NAMES.has(name)) {
return {
attribution: 'user',
errorCode,
hint: 'A workflow-only or step-only API was called from the wrong context. The error message includes the exact API and how to move the call.',
};
}

if (err instanceof WorkflowRuntimeError) {
return {
attribution: 'sdk',
errorCode,
hint: 'This is an internal workflow SDK error, not a bug in your code. If it keeps happening, please report it with the stack trace and the runId.',
};
}

if (errorCode === RUN_ERROR_CODES.REPLAY_TIMEOUT) {
return {
attribution: 'sdk',
errorCode,
hint: 'The workflow replay took too long. This usually means the event log is unusually large or the workflow function is doing heavy synchronous work between step boundaries.',
};
}

if (errorCode === RUN_ERROR_CODES.MAX_DELIVERIES_EXCEEDED) {
return {
attribution: 'sdk',
errorCode,
hint: 'The workflow queue exceeded its max-delivery budget. This usually indicates a persistent runtime failure — check the most recent stack traces for the underlying cause.',
};
}

CopilotAIApr 23, 2026

Copy link

Choose a reason for hiding this comment

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

describeError() currently derives errorCode exclusively via classifyRunError(), which only returns USER_ERROR or RUNTIME_ERROR. That makes the REPLAY_TIMEOUT and MAX_DELIVERIES_EXCEEDED branches unreachable, so the SDK attribution/hints for those codes will never be produced. Consider changing the API to accept a precomputed RunErrorCode (or introducing a describeErrorCode() helper) and updating call sites that have these codes but no thrown error to use it; add tests covering those paths.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Fixed in 3dd68ccdescribeError now takes an optional errorCode parameter (see the MAX_DELIVERIES_EXCEEDED / REPLAY_TIMEOUT call sites in runtime.ts). Callers that already know the code pass it in; the internal branches match on that precomputed code, so they're no longer unreachable. Three new tests cover the paths.

Comment threadpackages/core/src/describe-error.ts Outdated
Comment on lines +32 to +71
const CONTEXT_ERROR_NAMES = new Set([
'NotInWorkflowContextError',
'NotInStepContextError',
'NotInWorkflowOrStepContextError',
'UnavailableInWorkflowContextError',
]);

/**
* Describe an error for user-facing presentation. Purely informational —
* does not change any persisted event data or error classification used by
* the runtime.
*
* The attribution here is more nuanced than `classifyRunError`:
*
* - `SerializationError` is technically raised by the SDK, but it almost
* always points at something the caller did (passed a non-serializable
* value, didn't register a class). We attribute it to the user.
* - Context-violation errors (`NotInWorkflowContextError`, etc.) likewise
* describe a user mistake.
* - `WorkflowRuntimeError` (and subclasses like `StepNotRegisteredError`)
* indicates an internal SDK invariant broke — surface that as `sdk`.
*/
export function describeError(err: unknown): ErrorDescription {
const errorCode = classifyRunError(err);
const name = err instanceof Error ? err.name : undefined;

if (SerializationError.is(err)) {
return {
attribution: 'user',
errorCode,
hint: 'A value passed across a workflow/step boundary could not be serialized. See the error message for the offending path and the Learn More link for details.',
};
}

if (name && CONTEXT_ERROR_NAMES.has(name)) {
return {
attribution: 'user',
errorCode,
hint: 'A workflow-only or step-only API was called from the wrong context. The error message includes the exact API and how to move the call.',
};

CopilotAIApr 23, 2026

Copy link

Choose a reason for hiding this comment

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

describeError() detects context-violation errors by comparing err.name against a string set. Since the concrete context error classes live in ./context-errors.ts, using instanceof checks (or a shared type guard) would be more robust and would fail at compile-time if those classes are renamed, instead of silently losing attribution/hints.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Fixed in 3dd68ccdescribeError now detects context-violation errors with instanceof against the concrete classes (NotInWorkflowContextError et al.) rather than a string Set of names, so renames can't silently break the classification.

Comment on lines 578 to 595
const errorCode = classifyRunError(err);
const description = describeError(err);
const framing =
description.attribution === 'sdk'
? `Workflow "${workflowName}" failed due to an SDK runtime error`
: `Workflow "${workflowName}" threw`;

// Use the stack as the primary message so it shows up
// in flattened logs without structured metadata.
runLogger.error(
errorStack || 'Unknown error encountered in workflow',
`${framing}\n${errorStack || 'Unknown error encountered in workflow'}`,
{
errorCode,
errorAttribution: description.attribution,
errorName,
errorMessage,
...(description.hint ? { hint: description.hint } : {}),
}

CopilotAIApr 23, 2026

Copy link

Choose a reason for hiding this comment

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

In this failure path errorCode is computed via classifyRunError(err) and describeError(err) recomputes it internally. Consider passing the already-computed errorCode into describeError (or returning attribution/hint separately) to avoid duplicated work and guarantee the logged errorCode and the description stay in sync if the classification logic evolves.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Addressed in 3dd68cc — the call site in runtime.ts now passes the precomputed errorCode into describeError(err, errorCode), so classifyRunError is only called once.

@@ -0,0 +1,11 @@
---
'@workflow/core': patch

CopilotAIApr 23, 2026

Copy link

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 consistently use double quotes (YAML string). For consistency (and to match existing tooling expectations), switch this to "@workflow/core": patch.

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

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Fixed in 3dd68cc — single-quoted frontmatter switched to double-quoted.

@pranaygp

Copy link
Copy Markdown
ContributorAuthor

Stacked: #1838 — Phase 6, cosmetic consistency pass on remaining bare throws

pranaygpand others added 2 commits April 23, 2026 18:15
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.
@pranaygp
pranaygpforce-pushed the pranaygp/friendlier-errors-phase-5-attribution branch from eb171bb to 3dd68ccCompareApril 24, 2026 01:18
@changeset-bot

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 3dd68cc

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

This PR includes changesets to release 17 packages
NameType
@workflow/corePatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch
@workflow/aiPatch

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

@pranaygp

Copy link
Copy Markdown
ContributorAuthor

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

Sign up for freeto 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

@pranaygp
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' Presentation-only user vs SDK error attribution by pranaygp · Pull Request #1837 · vercel/workflow · GitHub
Skip to content

Presentation-only user vs SDK error attribution - #1837

Closed
pranaygp wants to merge 2 commits into
pranaygp/friendlier-errors-phase-4-serializationfrom
pranaygp/friendlier-errors-phase-5-attribution
Closed

Presentation-only user vs SDK error attribution#1837
pranaygp wants to merge 2 commits into
pranaygp/friendlier-errors-phase-4-serializationfrom
pranaygp/friendlier-errors-phase-5-attribution

Conversation

@pranaygp

@pranaygppranaygp commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Phase 5 of the friendlier-errors stack. Adds a presentation-only helper describeError() that computes user vs SDK attribution and class-aware hints from existing error classes and RUN_ERROR_CODES. No event data or persisted error classification is affected.

  • New packages/core/src/describe-error.ts returning { attribution, errorCode, hint? }
    • user for plain errors, SerializationError, and context-violation errors (NotInWorkflowContextError, etc.)
    • sdk for WorkflowRuntimeError (and subclasses like StepNotRegisteredError), replay timeouts, and max-delivery exhaustion
  • Terminal logs at step-failure, max-retries, run-failure, and fatal-setup sites now include errorAttribution metadata and a hint field for known error types.
  • 6 unit tests cover the attribution matrix.

Motivated by a real customer case where a vendor's 503 looked like an SDK failure.

Manual test plan

Using workbench/nextjs-turbopackpnpm dev and watch the terminal. Each test checks the structured metadata in the [workflow-sdk] log at step/run-failure time.

  • User error → attribution: 'user', no hint — throw plain Error('boom') from a step. Confirm the log metadata includes errorAttribution: 'user' and nohint field (plain user errors get no hint — stack is already useful).

  • SerializationError'user' + serialization hint — reuse the "unregistered class" test from Phase 4. Log metadata should include errorAttribution: 'user' and hint: 'A value passed across a workflow/step boundary could not be serialized. See the error message for the offending path and the Learn More link for details.'

  • Context-violation → 'user' + context hint — call createHook() from a route. Log metadata should include errorAttribution: 'user' and hint: 'A workflow-only or step-only API was called from the wrong context. The error message includes the exact API and how to move the call.'

  • WorkflowRuntimeError'sdk' + runtime hint — hard to trigger accidentally; throw one directly:

    import{WorkflowRuntimeError}from'@workflow/errors';asyncfunctions(){'use step';thrownewWorkflowRuntimeError('invariant broken');}

    Expect errorAttribution: 'sdk' and hint: 'This is an internal workflow SDK error, not a bug in your code. If it keeps happening, please report it with the stack trace and the runId.'

  • Replay timeout → 'sdk' + replay hint — set WORKFLOW_REPLAY_TIMEOUT_MS=50, run a non-trivial workflow. After retries exhaust, expect errorAttribution: 'sdk' and hint: 'The workflow replay took too long…'.

  • Max-delivery exhaustion → 'sdk' + max-delivery hint — write a step that always throws. After the queue's max-delivery budget exhausts, expect errorAttribution: 'sdk' and hint: 'The workflow queue exceeded its max-delivery budget…'.

  • Attribution applies at all four log sites — confirm the log contains errorAttribution + optional hint at:

    • step failure (non-terminal)
    • step max-retries (terminal)
    • run failure
    • fatal workflow setup

Unit tests

  • pnpm --filter @workflow/core exec vitest run src/describe-error.test.ts (6 tests)
  • Step-handler / runtime log-site changes don't affect classify-error.test.ts

📚 Friendlier errors stack

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

#PRPhaseSummary
1#1831Phase 1 + 2Ansi rendering primitives + context-violation errors
2#1832Phase 3Structured logger metadata; folds in #1812
3#1836Phase 4SerializationError at serialization / stream / encryption boundaries
4→ this PR (#1837)Phase 5Presentation-only user vs SDK attribution (describeError)
5#1838Phase 6Consistency pass on remaining bare throw new Error(...) sites
6#1839Phase 7 foundationData-driven describeRunError + public subpath
7#1840Phase 8WorkflowBuildError + applications in @workflow/builders
8#1849FollowupsDrop 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

CopilotAI review requested due to automatic review settings April 23, 2026 16:40
@vercel

vercelBot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
❌ 💻 Local Development10522861140
✅ 📦 Local Production10540861140
✅ 🐘 Local Postgres10540861140
✅ 📋 Other267018285
Total342722763705

❌ 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
AppPassedFailedSkipped
✅ astro-stable8906
✅ express-stable8906
✅ fastify-stable8906
✅ hono-stable8906
✅ nextjs-turbopack-canary76019
✅ nextjs-turbopack-stable9500
✅ nextjs-webpack-canary76019
✅ nextjs-webpack-stable9500
✅ nitro-stable8906
✅ nuxt-stable8906
✅ sveltekit-stable8906
❌ vite-stable8726
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable8906
✅ express-stable8906
✅ fastify-stable8906
✅ hono-stable8906
✅ nextjs-turbopack-canary76019
✅ nextjs-turbopack-stable9500
✅ nextjs-webpack-canary76019
✅ nextjs-webpack-stable9500
✅ nitro-stable8906
✅ nuxt-stable8906
✅ sveltekit-stable8906
✅ vite-stable8906
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable8906
✅ express-stable8906
✅ fastify-stable8906
✅ hono-stable8906
✅ nextjs-turbopack-canary76019
✅ nextjs-turbopack-stable9500
✅ nextjs-webpack-canary76019
✅ nextjs-webpack-stable9500
✅ nitro-stable8906
✅ nuxt-stable8906
✅ sveltekit-stable8906
✅ vite-stable8906
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable8906
✅ e2e-local-postgres-nest-stable8906
✅ e2e-local-prod-nest-stable8906

📋 View full workflow run


Some E2E test jobs failed:

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

Check the workflow run for details.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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 new presentation-only error description helper in @workflow/core to enrich runtime/step terminal logs with user-vs-SDK attribution and (when available) class-aware hints, without changing persisted error classification or event data.

Changes:

  • Introduce describeError() returning { attribution, errorCode, hint? } based on existing error classes.
  • Add errorAttribution (and optional hint) metadata to step-failure and run-failure log sites.
  • Add unit tests for the attribution matrix and a changeset entry.

Reviewed changes

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

Show a summary per file
FileDescription
packages/core/src/runtime/step-handler.tsEnriches step failure/max-retry logs with attribution + optional hint.
packages/core/src/runtime.tsEnriches fatal-setup and run-failure logs with attribution + optional hint.
packages/core/src/describe-error.tsNew helper implementing attribution/hint logic.
packages/core/src/describe-error.test.tsUnit tests validating attribution for key error types.
.changeset/friendlier-error-attribution.mdRelease note for new helper + logging metadata changes.

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

Comment threadpackages/core/src/describe-error.ts Outdated
Comment on lines +54 to +96
export function describeError(err: unknown): ErrorDescription {
const errorCode = classifyRunError(err);
const name = err instanceof Error ? err.name : undefined;

if (SerializationError.is(err)) {
return {
attribution: 'user',
errorCode,
hint: 'A value passed across a workflow/step boundary could not be serialized. See the error message for the offending path and the Learn More link for details.',
};
}

if (name && CONTEXT_ERROR_NAMES.has(name)) {
return {
attribution: 'user',
errorCode,
hint: 'A workflow-only or step-only API was called from the wrong context. The error message includes the exact API and how to move the call.',
};
}

if (err instanceof WorkflowRuntimeError) {
return {
attribution: 'sdk',
errorCode,
hint: 'This is an internal workflow SDK error, not a bug in your code. If it keeps happening, please report it with the stack trace and the runId.',
};
}

if (errorCode === RUN_ERROR_CODES.REPLAY_TIMEOUT) {
return {
attribution: 'sdk',
errorCode,
hint: 'The workflow replay took too long. This usually means the event log is unusually large or the workflow function is doing heavy synchronous work between step boundaries.',
};
}

if (errorCode === RUN_ERROR_CODES.MAX_DELIVERIES_EXCEEDED) {
return {
attribution: 'sdk',
errorCode,
hint: 'The workflow queue exceeded its max-delivery budget. This usually indicates a persistent runtime failure — check the most recent stack traces for the underlying cause.',
};
}

CopilotAIApr 23, 2026

Copy link

Choose a reason for hiding this comment

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

describeError() currently derives errorCode exclusively via classifyRunError(), which only returns USER_ERROR or RUNTIME_ERROR. That makes the REPLAY_TIMEOUT and MAX_DELIVERIES_EXCEEDED branches unreachable, so the SDK attribution/hints for those codes will never be produced. Consider changing the API to accept a precomputed RunErrorCode (or introducing a describeErrorCode() helper) and updating call sites that have these codes but no thrown error to use it; add tests covering those paths.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Fixed in 3dd68ccdescribeError now takes an optional errorCode parameter (see the MAX_DELIVERIES_EXCEEDED / REPLAY_TIMEOUT call sites in runtime.ts). Callers that already know the code pass it in; the internal branches match on that precomputed code, so they're no longer unreachable. Three new tests cover the paths.

Comment threadpackages/core/src/describe-error.ts Outdated
Comment on lines +32 to +71
const CONTEXT_ERROR_NAMES = new Set([
'NotInWorkflowContextError',
'NotInStepContextError',
'NotInWorkflowOrStepContextError',
'UnavailableInWorkflowContextError',
]);

/**
* Describe an error for user-facing presentation. Purely informational —
* does not change any persisted event data or error classification used by
* the runtime.
*
* The attribution here is more nuanced than `classifyRunError`:
*
* - `SerializationError` is technically raised by the SDK, but it almost
* always points at something the caller did (passed a non-serializable
* value, didn't register a class). We attribute it to the user.
* - Context-violation errors (`NotInWorkflowContextError`, etc.) likewise
* describe a user mistake.
* - `WorkflowRuntimeError` (and subclasses like `StepNotRegisteredError`)
* indicates an internal SDK invariant broke — surface that as `sdk`.
*/
export function describeError(err: unknown): ErrorDescription {
const errorCode = classifyRunError(err);
const name = err instanceof Error ? err.name : undefined;

if (SerializationError.is(err)) {
return {
attribution: 'user',
errorCode,
hint: 'A value passed across a workflow/step boundary could not be serialized. See the error message for the offending path and the Learn More link for details.',
};
}

if (name && CONTEXT_ERROR_NAMES.has(name)) {
return {
attribution: 'user',
errorCode,
hint: 'A workflow-only or step-only API was called from the wrong context. The error message includes the exact API and how to move the call.',
};

CopilotAIApr 23, 2026

Copy link

Choose a reason for hiding this comment

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

describeError() detects context-violation errors by comparing err.name against a string set. Since the concrete context error classes live in ./context-errors.ts, using instanceof checks (or a shared type guard) would be more robust and would fail at compile-time if those classes are renamed, instead of silently losing attribution/hints.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Fixed in 3dd68ccdescribeError now detects context-violation errors with instanceof against the concrete classes (NotInWorkflowContextError et al.) rather than a string Set of names, so renames can't silently break the classification.

Comment on lines 578 to 595
const errorCode = classifyRunError(err);
const description = describeError(err);
const framing =
description.attribution === 'sdk'
? `Workflow "${workflowName}" failed due to an SDK runtime error`
: `Workflow "${workflowName}" threw`;

// Use the stack as the primary message so it shows up
// in flattened logs without structured metadata.
runLogger.error(
errorStack || 'Unknown error encountered in workflow',
`${framing}\n${errorStack || 'Unknown error encountered in workflow'}`,
{
errorCode,
errorAttribution: description.attribution,
errorName,
errorMessage,
...(description.hint ? { hint: description.hint } : {}),
}

CopilotAIApr 23, 2026

Copy link

Choose a reason for hiding this comment

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

In this failure path errorCode is computed via classifyRunError(err) and describeError(err) recomputes it internally. Consider passing the already-computed errorCode into describeError (or returning attribution/hint separately) to avoid duplicated work and guarantee the logged errorCode and the description stay in sync if the classification logic evolves.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Addressed in 3dd68cc — the call site in runtime.ts now passes the precomputed errorCode into describeError(err, errorCode), so classifyRunError is only called once.

@@ -0,0 +1,11 @@
---
'@workflow/core': patch

CopilotAIApr 23, 2026

Copy link

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 consistently use double quotes (YAML string). For consistency (and to match existing tooling expectations), switch this to "@workflow/core": patch.

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

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Fixed in 3dd68cc — single-quoted frontmatter switched to double-quoted.

@pranaygp

Copy link
Copy Markdown
ContributorAuthor

Stacked: #1838 — Phase 6, cosmetic consistency pass on remaining bare throws

pranaygpand others added 2 commits April 23, 2026 18:15
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.
@pranaygp
pranaygpforce-pushed the pranaygp/friendlier-errors-phase-5-attribution branch from eb171bb to 3dd68ccCompareApril 24, 2026 01:18
@changeset-bot

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 3dd68cc

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

This PR includes changesets to release 17 packages
NameType
@workflow/corePatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch
@workflow/aiPatch

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

@pranaygp

Copy link
Copy Markdown
ContributorAuthor

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

Sign up for freeto 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

@pranaygp
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Presentation-only user vs SDK error attribution by pranaygp · Pull Request #1837 · vercel/workflow · GitHub
Skip to content

Presentation-only user vs SDK error attribution - #1837

Closed
pranaygp wants to merge 2 commits into
pranaygp/friendlier-errors-phase-4-serializationfrom
pranaygp/friendlier-errors-phase-5-attribution
Closed

Presentation-only user vs SDK error attribution#1837
pranaygp wants to merge 2 commits into
pranaygp/friendlier-errors-phase-4-serializationfrom
pranaygp/friendlier-errors-phase-5-attribution

Conversation

@pranaygp

@pranaygppranaygp commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Phase 5 of the friendlier-errors stack. Adds a presentation-only helper describeError() that computes user vs SDK attribution and class-aware hints from existing error classes and RUN_ERROR_CODES. No event data or persisted error classification is affected.

  • New packages/core/src/describe-error.ts returning { attribution, errorCode, hint? }
    • user for plain errors, SerializationError, and context-violation errors (NotInWorkflowContextError, etc.)
    • sdk for WorkflowRuntimeError (and subclasses like StepNotRegisteredError), replay timeouts, and max-delivery exhaustion
  • Terminal logs at step-failure, max-retries, run-failure, and fatal-setup sites now include errorAttribution metadata and a hint field for known error types.
  • 6 unit tests cover the attribution matrix.

Motivated by a real customer case where a vendor's 503 looked like an SDK failure.

Manual test plan

Using workbench/nextjs-turbopackpnpm dev and watch the terminal. Each test checks the structured metadata in the [workflow-sdk] log at step/run-failure time.

  • User error → attribution: 'user', no hint — throw plain Error('boom') from a step. Confirm the log metadata includes errorAttribution: 'user' and nohint field (plain user errors get no hint — stack is already useful).

  • SerializationError'user' + serialization hint — reuse the "unregistered class" test from Phase 4. Log metadata should include errorAttribution: 'user' and hint: 'A value passed across a workflow/step boundary could not be serialized. See the error message for the offending path and the Learn More link for details.'

  • Context-violation → 'user' + context hint — call createHook() from a route. Log metadata should include errorAttribution: 'user' and hint: 'A workflow-only or step-only API was called from the wrong context. The error message includes the exact API and how to move the call.'

  • WorkflowRuntimeError'sdk' + runtime hint — hard to trigger accidentally; throw one directly:

    import{WorkflowRuntimeError}from'@workflow/errors';asyncfunctions(){'use step';thrownewWorkflowRuntimeError('invariant broken');}

    Expect errorAttribution: 'sdk' and hint: 'This is an internal workflow SDK error, not a bug in your code. If it keeps happening, please report it with the stack trace and the runId.'

  • Replay timeout → 'sdk' + replay hint — set WORKFLOW_REPLAY_TIMEOUT_MS=50, run a non-trivial workflow. After retries exhaust, expect errorAttribution: 'sdk' and hint: 'The workflow replay took too long…'.

  • Max-delivery exhaustion → 'sdk' + max-delivery hint — write a step that always throws. After the queue's max-delivery budget exhausts, expect errorAttribution: 'sdk' and hint: 'The workflow queue exceeded its max-delivery budget…'.

  • Attribution applies at all four log sites — confirm the log contains errorAttribution + optional hint at:

    • step failure (non-terminal)
    • step max-retries (terminal)
    • run failure
    • fatal workflow setup

Unit tests

  • pnpm --filter @workflow/core exec vitest run src/describe-error.test.ts (6 tests)
  • Step-handler / runtime log-site changes don't affect classify-error.test.ts

📚 Friendlier errors stack

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

#PRPhaseSummary
1#1831Phase 1 + 2Ansi rendering primitives + context-violation errors
2#1832Phase 3Structured logger metadata; folds in #1812
3#1836Phase 4SerializationError at serialization / stream / encryption boundaries
4→ this PR (#1837)Phase 5Presentation-only user vs SDK attribution (describeError)
5#1838Phase 6Consistency pass on remaining bare throw new Error(...) sites
6#1839Phase 7 foundationData-driven describeRunError + public subpath
7#1840Phase 8WorkflowBuildError + applications in @workflow/builders
8#1849FollowupsDrop 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

CopilotAI review requested due to automatic review settings April 23, 2026 16:40
@vercel

vercelBot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
❌ 💻 Local Development10522861140
✅ 📦 Local Production10540861140
✅ 🐘 Local Postgres10540861140
✅ 📋 Other267018285
Total342722763705

❌ 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
AppPassedFailedSkipped
✅ astro-stable8906
✅ express-stable8906
✅ fastify-stable8906
✅ hono-stable8906
✅ nextjs-turbopack-canary76019
✅ nextjs-turbopack-stable9500
✅ nextjs-webpack-canary76019
✅ nextjs-webpack-stable9500
✅ nitro-stable8906
✅ nuxt-stable8906
✅ sveltekit-stable8906
❌ vite-stable8726
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable8906
✅ express-stable8906
✅ fastify-stable8906
✅ hono-stable8906
✅ nextjs-turbopack-canary76019
✅ nextjs-turbopack-stable9500
✅ nextjs-webpack-canary76019
✅ nextjs-webpack-stable9500
✅ nitro-stable8906
✅ nuxt-stable8906
✅ sveltekit-stable8906
✅ vite-stable8906
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable8906
✅ express-stable8906
✅ fastify-stable8906
✅ hono-stable8906
✅ nextjs-turbopack-canary76019
✅ nextjs-turbopack-stable9500
✅ nextjs-webpack-canary76019
✅ nextjs-webpack-stable9500
✅ nitro-stable8906
✅ nuxt-stable8906
✅ sveltekit-stable8906
✅ vite-stable8906
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable8906
✅ e2e-local-postgres-nest-stable8906
✅ e2e-local-prod-nest-stable8906

📋 View full workflow run


Some E2E test jobs failed:

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

Check the workflow run for details.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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 new presentation-only error description helper in @workflow/core to enrich runtime/step terminal logs with user-vs-SDK attribution and (when available) class-aware hints, without changing persisted error classification or event data.

Changes:

  • Introduce describeError() returning { attribution, errorCode, hint? } based on existing error classes.
  • Add errorAttribution (and optional hint) metadata to step-failure and run-failure log sites.
  • Add unit tests for the attribution matrix and a changeset entry.

Reviewed changes

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

Show a summary per file
FileDescription
packages/core/src/runtime/step-handler.tsEnriches step failure/max-retry logs with attribution + optional hint.
packages/core/src/runtime.tsEnriches fatal-setup and run-failure logs with attribution + optional hint.
packages/core/src/describe-error.tsNew helper implementing attribution/hint logic.
packages/core/src/describe-error.test.tsUnit tests validating attribution for key error types.
.changeset/friendlier-error-attribution.mdRelease note for new helper + logging metadata changes.

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

Comment threadpackages/core/src/describe-error.ts Outdated
Comment on lines +54 to +96
export function describeError(err: unknown): ErrorDescription {
const errorCode = classifyRunError(err);
const name = err instanceof Error ? err.name : undefined;

if (SerializationError.is(err)) {
return {
attribution: 'user',
errorCode,
hint: 'A value passed across a workflow/step boundary could not be serialized. See the error message for the offending path and the Learn More link for details.',
};
}

if (name && CONTEXT_ERROR_NAMES.has(name)) {
return {
attribution: 'user',
errorCode,
hint: 'A workflow-only or step-only API was called from the wrong context. The error message includes the exact API and how to move the call.',
};
}

if (err instanceof WorkflowRuntimeError) {
return {
attribution: 'sdk',
errorCode,
hint: 'This is an internal workflow SDK error, not a bug in your code. If it keeps happening, please report it with the stack trace and the runId.',
};
}

if (errorCode === RUN_ERROR_CODES.REPLAY_TIMEOUT) {
return {
attribution: 'sdk',
errorCode,
hint: 'The workflow replay took too long. This usually means the event log is unusually large or the workflow function is doing heavy synchronous work between step boundaries.',
};
}

if (errorCode === RUN_ERROR_CODES.MAX_DELIVERIES_EXCEEDED) {
return {
attribution: 'sdk',
errorCode,
hint: 'The workflow queue exceeded its max-delivery budget. This usually indicates a persistent runtime failure — check the most recent stack traces for the underlying cause.',
};
}

CopilotAIApr 23, 2026

Copy link

Choose a reason for hiding this comment

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

describeError() currently derives errorCode exclusively via classifyRunError(), which only returns USER_ERROR or RUNTIME_ERROR. That makes the REPLAY_TIMEOUT and MAX_DELIVERIES_EXCEEDED branches unreachable, so the SDK attribution/hints for those codes will never be produced. Consider changing the API to accept a precomputed RunErrorCode (or introducing a describeErrorCode() helper) and updating call sites that have these codes but no thrown error to use it; add tests covering those paths.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Fixed in 3dd68ccdescribeError now takes an optional errorCode parameter (see the MAX_DELIVERIES_EXCEEDED / REPLAY_TIMEOUT call sites in runtime.ts). Callers that already know the code pass it in; the internal branches match on that precomputed code, so they're no longer unreachable. Three new tests cover the paths.

Comment threadpackages/core/src/describe-error.ts Outdated
Comment on lines +32 to +71
const CONTEXT_ERROR_NAMES = new Set([
'NotInWorkflowContextError',
'NotInStepContextError',
'NotInWorkflowOrStepContextError',
'UnavailableInWorkflowContextError',
]);

/**
* Describe an error for user-facing presentation. Purely informational —
* does not change any persisted event data or error classification used by
* the runtime.
*
* The attribution here is more nuanced than `classifyRunError`:
*
* - `SerializationError` is technically raised by the SDK, but it almost
* always points at something the caller did (passed a non-serializable
* value, didn't register a class). We attribute it to the user.
* - Context-violation errors (`NotInWorkflowContextError`, etc.) likewise
* describe a user mistake.
* - `WorkflowRuntimeError` (and subclasses like `StepNotRegisteredError`)
* indicates an internal SDK invariant broke — surface that as `sdk`.
*/
export function describeError(err: unknown): ErrorDescription {
const errorCode = classifyRunError(err);
const name = err instanceof Error ? err.name : undefined;

if (SerializationError.is(err)) {
return {
attribution: 'user',
errorCode,
hint: 'A value passed across a workflow/step boundary could not be serialized. See the error message for the offending path and the Learn More link for details.',
};
}

if (name && CONTEXT_ERROR_NAMES.has(name)) {
return {
attribution: 'user',
errorCode,
hint: 'A workflow-only or step-only API was called from the wrong context. The error message includes the exact API and how to move the call.',
};

CopilotAIApr 23, 2026

Copy link

Choose a reason for hiding this comment

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

describeError() detects context-violation errors by comparing err.name against a string set. Since the concrete context error classes live in ./context-errors.ts, using instanceof checks (or a shared type guard) would be more robust and would fail at compile-time if those classes are renamed, instead of silently losing attribution/hints.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Fixed in 3dd68ccdescribeError now detects context-violation errors with instanceof against the concrete classes (NotInWorkflowContextError et al.) rather than a string Set of names, so renames can't silently break the classification.

Comment on lines 578 to 595
const errorCode = classifyRunError(err);
const description = describeError(err);
const framing =
description.attribution === 'sdk'
? `Workflow "${workflowName}" failed due to an SDK runtime error`
: `Workflow "${workflowName}" threw`;

// Use the stack as the primary message so it shows up
// in flattened logs without structured metadata.
runLogger.error(
errorStack || 'Unknown error encountered in workflow',
`${framing}\n${errorStack || 'Unknown error encountered in workflow'}`,
{
errorCode,
errorAttribution: description.attribution,
errorName,
errorMessage,
...(description.hint ? { hint: description.hint } : {}),
}

CopilotAIApr 23, 2026

Copy link

Choose a reason for hiding this comment

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

In this failure path errorCode is computed via classifyRunError(err) and describeError(err) recomputes it internally. Consider passing the already-computed errorCode into describeError (or returning attribution/hint separately) to avoid duplicated work and guarantee the logged errorCode and the description stay in sync if the classification logic evolves.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Addressed in 3dd68cc — the call site in runtime.ts now passes the precomputed errorCode into describeError(err, errorCode), so classifyRunError is only called once.

@@ -0,0 +1,11 @@
---
'@workflow/core': patch

CopilotAIApr 23, 2026

Copy link

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 consistently use double quotes (YAML string). For consistency (and to match existing tooling expectations), switch this to "@workflow/core": patch.

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

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Fixed in 3dd68cc — single-quoted frontmatter switched to double-quoted.

@pranaygp

Copy link
Copy Markdown
ContributorAuthor

Stacked: #1838 — Phase 6, cosmetic consistency pass on remaining bare throws

pranaygpand others added 2 commits April 23, 2026 18:15
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.
@pranaygp
pranaygpforce-pushed the pranaygp/friendlier-errors-phase-5-attribution branch from eb171bb to 3dd68ccCompareApril 24, 2026 01:18
@changeset-bot

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 3dd68cc

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

This PR includes changesets to release 17 packages
NameType
@workflow/corePatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch
@workflow/aiPatch

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

@pranaygp

Copy link
Copy Markdown
ContributorAuthor

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

Sign up for freeto 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

@pranaygp
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' Presentation-only user vs SDK error attribution by pranaygp · Pull Request #1837 · vercel/workflow · GitHub
Skip to content

Presentation-only user vs SDK error attribution - #1837

Closed
pranaygp wants to merge 2 commits into
pranaygp/friendlier-errors-phase-4-serializationfrom
pranaygp/friendlier-errors-phase-5-attribution
Closed

Presentation-only user vs SDK error attribution#1837
pranaygp wants to merge 2 commits into
pranaygp/friendlier-errors-phase-4-serializationfrom
pranaygp/friendlier-errors-phase-5-attribution

Conversation

@pranaygp

@pranaygppranaygp commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Phase 5 of the friendlier-errors stack. Adds a presentation-only helper describeError() that computes user vs SDK attribution and class-aware hints from existing error classes and RUN_ERROR_CODES. No event data or persisted error classification is affected.

  • New packages/core/src/describe-error.ts returning { attribution, errorCode, hint? }
    • user for plain errors, SerializationError, and context-violation errors (NotInWorkflowContextError, etc.)
    • sdk for WorkflowRuntimeError (and subclasses like StepNotRegisteredError), replay timeouts, and max-delivery exhaustion
  • Terminal logs at step-failure, max-retries, run-failure, and fatal-setup sites now include errorAttribution metadata and a hint field for known error types.
  • 6 unit tests cover the attribution matrix.

Motivated by a real customer case where a vendor's 503 looked like an SDK failure.

Manual test plan

Using workbench/nextjs-turbopackpnpm dev and watch the terminal. Each test checks the structured metadata in the [workflow-sdk] log at step/run-failure time.

  • User error → attribution: 'user', no hint — throw plain Error('boom') from a step. Confirm the log metadata includes errorAttribution: 'user' and nohint field (plain user errors get no hint — stack is already useful).

  • SerializationError'user' + serialization hint — reuse the "unregistered class" test from Phase 4. Log metadata should include errorAttribution: 'user' and hint: 'A value passed across a workflow/step boundary could not be serialized. See the error message for the offending path and the Learn More link for details.'

  • Context-violation → 'user' + context hint — call createHook() from a route. Log metadata should include errorAttribution: 'user' and hint: 'A workflow-only or step-only API was called from the wrong context. The error message includes the exact API and how to move the call.'

  • WorkflowRuntimeError'sdk' + runtime hint — hard to trigger accidentally; throw one directly:

    import{WorkflowRuntimeError}from'@workflow/errors';asyncfunctions(){'use step';thrownewWorkflowRuntimeError('invariant broken');}

    Expect errorAttribution: 'sdk' and hint: 'This is an internal workflow SDK error, not a bug in your code. If it keeps happening, please report it with the stack trace and the runId.'

  • Replay timeout → 'sdk' + replay hint — set WORKFLOW_REPLAY_TIMEOUT_MS=50, run a non-trivial workflow. After retries exhaust, expect errorAttribution: 'sdk' and hint: 'The workflow replay took too long…'.

  • Max-delivery exhaustion → 'sdk' + max-delivery hint — write a step that always throws. After the queue's max-delivery budget exhausts, expect errorAttribution: 'sdk' and hint: 'The workflow queue exceeded its max-delivery budget…'.

  • Attribution applies at all four log sites — confirm the log contains errorAttribution + optional hint at:

    • step failure (non-terminal)
    • step max-retries (terminal)
    • run failure
    • fatal workflow setup

Unit tests

  • pnpm --filter @workflow/core exec vitest run src/describe-error.test.ts (6 tests)
  • Step-handler / runtime log-site changes don't affect classify-error.test.ts

📚 Friendlier errors stack

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

#PRPhaseSummary
1#1831Phase 1 + 2Ansi rendering primitives + context-violation errors
2#1832Phase 3Structured logger metadata; folds in #1812
3#1836Phase 4SerializationError at serialization / stream / encryption boundaries
4→ this PR (#1837)Phase 5Presentation-only user vs SDK attribution (describeError)
5#1838Phase 6Consistency pass on remaining bare throw new Error(...) sites
6#1839Phase 7 foundationData-driven describeRunError + public subpath
7#1840Phase 8WorkflowBuildError + applications in @workflow/builders
8#1849FollowupsDrop 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

CopilotAI review requested due to automatic review settings April 23, 2026 16:40
@vercel

vercelBot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
❌ 💻 Local Development10522861140
✅ 📦 Local Production10540861140
✅ 🐘 Local Postgres10540861140
✅ 📋 Other267018285
Total342722763705

❌ 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
AppPassedFailedSkipped
✅ astro-stable8906
✅ express-stable8906
✅ fastify-stable8906
✅ hono-stable8906
✅ nextjs-turbopack-canary76019
✅ nextjs-turbopack-stable9500
✅ nextjs-webpack-canary76019
✅ nextjs-webpack-stable9500
✅ nitro-stable8906
✅ nuxt-stable8906
✅ sveltekit-stable8906
❌ vite-stable8726
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable8906
✅ express-stable8906
✅ fastify-stable8906
✅ hono-stable8906
✅ nextjs-turbopack-canary76019
✅ nextjs-turbopack-stable9500
✅ nextjs-webpack-canary76019
✅ nextjs-webpack-stable9500
✅ nitro-stable8906
✅ nuxt-stable8906
✅ sveltekit-stable8906
✅ vite-stable8906
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable8906
✅ express-stable8906
✅ fastify-stable8906
✅ hono-stable8906
✅ nextjs-turbopack-canary76019
✅ nextjs-turbopack-stable9500
✅ nextjs-webpack-canary76019
✅ nextjs-webpack-stable9500
✅ nitro-stable8906
✅ nuxt-stable8906
✅ sveltekit-stable8906
✅ vite-stable8906
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable8906
✅ e2e-local-postgres-nest-stable8906
✅ e2e-local-prod-nest-stable8906

📋 View full workflow run


Some E2E test jobs failed:

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

Check the workflow run for details.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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 new presentation-only error description helper in @workflow/core to enrich runtime/step terminal logs with user-vs-SDK attribution and (when available) class-aware hints, without changing persisted error classification or event data.

Changes:

  • Introduce describeError() returning { attribution, errorCode, hint? } based on existing error classes.
  • Add errorAttribution (and optional hint) metadata to step-failure and run-failure log sites.
  • Add unit tests for the attribution matrix and a changeset entry.

Reviewed changes

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

Show a summary per file
FileDescription
packages/core/src/runtime/step-handler.tsEnriches step failure/max-retry logs with attribution + optional hint.
packages/core/src/runtime.tsEnriches fatal-setup and run-failure logs with attribution + optional hint.
packages/core/src/describe-error.tsNew helper implementing attribution/hint logic.
packages/core/src/describe-error.test.tsUnit tests validating attribution for key error types.
.changeset/friendlier-error-attribution.mdRelease note for new helper + logging metadata changes.

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

Comment threadpackages/core/src/describe-error.ts Outdated
Comment on lines +54 to +96
export function describeError(err: unknown): ErrorDescription {
const errorCode = classifyRunError(err);
const name = err instanceof Error ? err.name : undefined;

if (SerializationError.is(err)) {
return {
attribution: 'user',
errorCode,
hint: 'A value passed across a workflow/step boundary could not be serialized. See the error message for the offending path and the Learn More link for details.',
};
}

if (name && CONTEXT_ERROR_NAMES.has(name)) {
return {
attribution: 'user',
errorCode,
hint: 'A workflow-only or step-only API was called from the wrong context. The error message includes the exact API and how to move the call.',
};
}

if (err instanceof WorkflowRuntimeError) {
return {
attribution: 'sdk',
errorCode,
hint: 'This is an internal workflow SDK error, not a bug in your code. If it keeps happening, please report it with the stack trace and the runId.',
};
}

if (errorCode === RUN_ERROR_CODES.REPLAY_TIMEOUT) {
return {
attribution: 'sdk',
errorCode,
hint: 'The workflow replay took too long. This usually means the event log is unusually large or the workflow function is doing heavy synchronous work between step boundaries.',
};
}

if (errorCode === RUN_ERROR_CODES.MAX_DELIVERIES_EXCEEDED) {
return {
attribution: 'sdk',
errorCode,
hint: 'The workflow queue exceeded its max-delivery budget. This usually indicates a persistent runtime failure — check the most recent stack traces for the underlying cause.',
};
}

CopilotAIApr 23, 2026

Copy link

Choose a reason for hiding this comment

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

describeError() currently derives errorCode exclusively via classifyRunError(), which only returns USER_ERROR or RUNTIME_ERROR. That makes the REPLAY_TIMEOUT and MAX_DELIVERIES_EXCEEDED branches unreachable, so the SDK attribution/hints for those codes will never be produced. Consider changing the API to accept a precomputed RunErrorCode (or introducing a describeErrorCode() helper) and updating call sites that have these codes but no thrown error to use it; add tests covering those paths.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Fixed in 3dd68ccdescribeError now takes an optional errorCode parameter (see the MAX_DELIVERIES_EXCEEDED / REPLAY_TIMEOUT call sites in runtime.ts). Callers that already know the code pass it in; the internal branches match on that precomputed code, so they're no longer unreachable. Three new tests cover the paths.

Comment threadpackages/core/src/describe-error.ts Outdated
Comment on lines +32 to +71
const CONTEXT_ERROR_NAMES = new Set([
'NotInWorkflowContextError',
'NotInStepContextError',
'NotInWorkflowOrStepContextError',
'UnavailableInWorkflowContextError',
]);

/**
* Describe an error for user-facing presentation. Purely informational —
* does not change any persisted event data or error classification used by
* the runtime.
*
* The attribution here is more nuanced than `classifyRunError`:
*
* - `SerializationError` is technically raised by the SDK, but it almost
* always points at something the caller did (passed a non-serializable
* value, didn't register a class). We attribute it to the user.
* - Context-violation errors (`NotInWorkflowContextError`, etc.) likewise
* describe a user mistake.
* - `WorkflowRuntimeError` (and subclasses like `StepNotRegisteredError`)
* indicates an internal SDK invariant broke — surface that as `sdk`.
*/
export function describeError(err: unknown): ErrorDescription {
const errorCode = classifyRunError(err);
const name = err instanceof Error ? err.name : undefined;

if (SerializationError.is(err)) {
return {
attribution: 'user',
errorCode,
hint: 'A value passed across a workflow/step boundary could not be serialized. See the error message for the offending path and the Learn More link for details.',
};
}

if (name && CONTEXT_ERROR_NAMES.has(name)) {
return {
attribution: 'user',
errorCode,
hint: 'A workflow-only or step-only API was called from the wrong context. The error message includes the exact API and how to move the call.',
};

CopilotAIApr 23, 2026

Copy link

Choose a reason for hiding this comment

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

describeError() detects context-violation errors by comparing err.name against a string set. Since the concrete context error classes live in ./context-errors.ts, using instanceof checks (or a shared type guard) would be more robust and would fail at compile-time if those classes are renamed, instead of silently losing attribution/hints.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Fixed in 3dd68ccdescribeError now detects context-violation errors with instanceof against the concrete classes (NotInWorkflowContextError et al.) rather than a string Set of names, so renames can't silently break the classification.

Comment on lines 578 to 595
const errorCode = classifyRunError(err);
const description = describeError(err);
const framing =
description.attribution === 'sdk'
? `Workflow "${workflowName}" failed due to an SDK runtime error`
: `Workflow "${workflowName}" threw`;

// Use the stack as the primary message so it shows up
// in flattened logs without structured metadata.
runLogger.error(
errorStack || 'Unknown error encountered in workflow',
`${framing}\n${errorStack || 'Unknown error encountered in workflow'}`,
{
errorCode,
errorAttribution: description.attribution,
errorName,
errorMessage,
...(description.hint ? { hint: description.hint } : {}),
}

CopilotAIApr 23, 2026

Copy link

Choose a reason for hiding this comment

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

In this failure path errorCode is computed via classifyRunError(err) and describeError(err) recomputes it internally. Consider passing the already-computed errorCode into describeError (or returning attribution/hint separately) to avoid duplicated work and guarantee the logged errorCode and the description stay in sync if the classification logic evolves.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Addressed in 3dd68cc — the call site in runtime.ts now passes the precomputed errorCode into describeError(err, errorCode), so classifyRunError is only called once.

@@ -0,0 +1,11 @@
---
'@workflow/core': patch

CopilotAIApr 23, 2026

Copy link

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 consistently use double quotes (YAML string). For consistency (and to match existing tooling expectations), switch this to "@workflow/core": patch.

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

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Fixed in 3dd68cc — single-quoted frontmatter switched to double-quoted.

@pranaygp

Copy link
Copy Markdown
ContributorAuthor

Stacked: #1838 — Phase 6, cosmetic consistency pass on remaining bare throws

pranaygpand others added 2 commits April 23, 2026 18:15
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.
@pranaygp
pranaygpforce-pushed the pranaygp/friendlier-errors-phase-5-attribution branch from eb171bb to 3dd68ccCompareApril 24, 2026 01:18
@changeset-bot

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 3dd68cc

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

This PR includes changesets to release 17 packages
NameType
@workflow/corePatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch
@workflow/aiPatch

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

@pranaygp

Copy link
Copy Markdown
ContributorAuthor

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

Sign up for freeto 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

@pranaygp
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); Presentation-only user vs SDK error attribution by pranaygp · Pull Request #1837 · vercel/workflow · GitHub
Skip to content

Presentation-only user vs SDK error attribution - #1837

Closed
pranaygp wants to merge 2 commits into
pranaygp/friendlier-errors-phase-4-serializationfrom
pranaygp/friendlier-errors-phase-5-attribution
Closed

Presentation-only user vs SDK error attribution#1837
pranaygp wants to merge 2 commits into
pranaygp/friendlier-errors-phase-4-serializationfrom
pranaygp/friendlier-errors-phase-5-attribution

Conversation

@pranaygp

@pranaygppranaygp commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Phase 5 of the friendlier-errors stack. Adds a presentation-only helper describeError() that computes user vs SDK attribution and class-aware hints from existing error classes and RUN_ERROR_CODES. No event data or persisted error classification is affected.

  • New packages/core/src/describe-error.ts returning { attribution, errorCode, hint? }
    • user for plain errors, SerializationError, and context-violation errors (NotInWorkflowContextError, etc.)
    • sdk for WorkflowRuntimeError (and subclasses like StepNotRegisteredError), replay timeouts, and max-delivery exhaustion
  • Terminal logs at step-failure, max-retries, run-failure, and fatal-setup sites now include errorAttribution metadata and a hint field for known error types.
  • 6 unit tests cover the attribution matrix.

Motivated by a real customer case where a vendor's 503 looked like an SDK failure.

Manual test plan

Using workbench/nextjs-turbopackpnpm dev and watch the terminal. Each test checks the structured metadata in the [workflow-sdk] log at step/run-failure time.

  • User error → attribution: 'user', no hint — throw plain Error('boom') from a step. Confirm the log metadata includes errorAttribution: 'user' and nohint field (plain user errors get no hint — stack is already useful).

  • SerializationError'user' + serialization hint — reuse the "unregistered class" test from Phase 4. Log metadata should include errorAttribution: 'user' and hint: 'A value passed across a workflow/step boundary could not be serialized. See the error message for the offending path and the Learn More link for details.'

  • Context-violation → 'user' + context hint — call createHook() from a route. Log metadata should include errorAttribution: 'user' and hint: 'A workflow-only or step-only API was called from the wrong context. The error message includes the exact API and how to move the call.'

  • WorkflowRuntimeError'sdk' + runtime hint — hard to trigger accidentally; throw one directly:

    import{WorkflowRuntimeError}from'@workflow/errors';asyncfunctions(){'use step';thrownewWorkflowRuntimeError('invariant broken');}

    Expect errorAttribution: 'sdk' and hint: 'This is an internal workflow SDK error, not a bug in your code. If it keeps happening, please report it with the stack trace and the runId.'

  • Replay timeout → 'sdk' + replay hint — set WORKFLOW_REPLAY_TIMEOUT_MS=50, run a non-trivial workflow. After retries exhaust, expect errorAttribution: 'sdk' and hint: 'The workflow replay took too long…'.

  • Max-delivery exhaustion → 'sdk' + max-delivery hint — write a step that always throws. After the queue's max-delivery budget exhausts, expect errorAttribution: 'sdk' and hint: 'The workflow queue exceeded its max-delivery budget…'.

  • Attribution applies at all four log sites — confirm the log contains errorAttribution + optional hint at:

    • step failure (non-terminal)
    • step max-retries (terminal)
    • run failure
    • fatal workflow setup

Unit tests

  • pnpm --filter @workflow/core exec vitest run src/describe-error.test.ts (6 tests)
  • Step-handler / runtime log-site changes don't affect classify-error.test.ts

📚 Friendlier errors stack

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

#PRPhaseSummary
1#1831Phase 1 + 2Ansi rendering primitives + context-violation errors
2#1832Phase 3Structured logger metadata; folds in #1812
3#1836Phase 4SerializationError at serialization / stream / encryption boundaries
4→ this PR (#1837)Phase 5Presentation-only user vs SDK attribution (describeError)
5#1838Phase 6Consistency pass on remaining bare throw new Error(...) sites
6#1839Phase 7 foundationData-driven describeRunError + public subpath
7#1840Phase 8WorkflowBuildError + applications in @workflow/builders
8#1849FollowupsDrop 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

CopilotAI review requested due to automatic review settings April 23, 2026 16:40
@vercel

vercelBot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actionsBot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

🧪 E2E Test Results

Some tests failed

Summary

PassedFailedSkippedTotal
❌ 💻 Local Development10522861140
✅ 📦 Local Production10540861140
✅ 🐘 Local Postgres10540861140
✅ 📋 Other267018285
Total342722763705

❌ 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
AppPassedFailedSkipped
✅ astro-stable8906
✅ express-stable8906
✅ fastify-stable8906
✅ hono-stable8906
✅ nextjs-turbopack-canary76019
✅ nextjs-turbopack-stable9500
✅ nextjs-webpack-canary76019
✅ nextjs-webpack-stable9500
✅ nitro-stable8906
✅ nuxt-stable8906
✅ sveltekit-stable8906
❌ vite-stable8726
✅ 📦 Local Production
AppPassedFailedSkipped
✅ astro-stable8906
✅ express-stable8906
✅ fastify-stable8906
✅ hono-stable8906
✅ nextjs-turbopack-canary76019
✅ nextjs-turbopack-stable9500
✅ nextjs-webpack-canary76019
✅ nextjs-webpack-stable9500
✅ nitro-stable8906
✅ nuxt-stable8906
✅ sveltekit-stable8906
✅ vite-stable8906
✅ 🐘 Local Postgres
AppPassedFailedSkipped
✅ astro-stable8906
✅ express-stable8906
✅ fastify-stable8906
✅ hono-stable8906
✅ nextjs-turbopack-canary76019
✅ nextjs-turbopack-stable9500
✅ nextjs-webpack-canary76019
✅ nextjs-webpack-stable9500
✅ nitro-stable8906
✅ nuxt-stable8906
✅ sveltekit-stable8906
✅ vite-stable8906
✅ 📋 Other
AppPassedFailedSkipped
✅ e2e-local-dev-nest-stable8906
✅ e2e-local-postgres-nest-stable8906
✅ e2e-local-prod-nest-stable8906

📋 View full workflow run


Some E2E test jobs failed:

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

Check the workflow run for details.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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 new presentation-only error description helper in @workflow/core to enrich runtime/step terminal logs with user-vs-SDK attribution and (when available) class-aware hints, without changing persisted error classification or event data.

Changes:

  • Introduce describeError() returning { attribution, errorCode, hint? } based on existing error classes.
  • Add errorAttribution (and optional hint) metadata to step-failure and run-failure log sites.
  • Add unit tests for the attribution matrix and a changeset entry.

Reviewed changes

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

Show a summary per file
FileDescription
packages/core/src/runtime/step-handler.tsEnriches step failure/max-retry logs with attribution + optional hint.
packages/core/src/runtime.tsEnriches fatal-setup and run-failure logs with attribution + optional hint.
packages/core/src/describe-error.tsNew helper implementing attribution/hint logic.
packages/core/src/describe-error.test.tsUnit tests validating attribution for key error types.
.changeset/friendlier-error-attribution.mdRelease note for new helper + logging metadata changes.

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

Comment threadpackages/core/src/describe-error.ts Outdated
Comment on lines +54 to +96
export function describeError(err: unknown): ErrorDescription {
const errorCode = classifyRunError(err);
const name = err instanceof Error ? err.name : undefined;

if (SerializationError.is(err)) {
return {
attribution: 'user',
errorCode,
hint: 'A value passed across a workflow/step boundary could not be serialized. See the error message for the offending path and the Learn More link for details.',
};
}

if (name && CONTEXT_ERROR_NAMES.has(name)) {
return {
attribution: 'user',
errorCode,
hint: 'A workflow-only or step-only API was called from the wrong context. The error message includes the exact API and how to move the call.',
};
}

if (err instanceof WorkflowRuntimeError) {
return {
attribution: 'sdk',
errorCode,
hint: 'This is an internal workflow SDK error, not a bug in your code. If it keeps happening, please report it with the stack trace and the runId.',
};
}

if (errorCode === RUN_ERROR_CODES.REPLAY_TIMEOUT) {
return {
attribution: 'sdk',
errorCode,
hint: 'The workflow replay took too long. This usually means the event log is unusually large or the workflow function is doing heavy synchronous work between step boundaries.',
};
}

if (errorCode === RUN_ERROR_CODES.MAX_DELIVERIES_EXCEEDED) {
return {
attribution: 'sdk',
errorCode,
hint: 'The workflow queue exceeded its max-delivery budget. This usually indicates a persistent runtime failure — check the most recent stack traces for the underlying cause.',
};
}

CopilotAIApr 23, 2026

Copy link

Choose a reason for hiding this comment

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

describeError() currently derives errorCode exclusively via classifyRunError(), which only returns USER_ERROR or RUNTIME_ERROR. That makes the REPLAY_TIMEOUT and MAX_DELIVERIES_EXCEEDED branches unreachable, so the SDK attribution/hints for those codes will never be produced. Consider changing the API to accept a precomputed RunErrorCode (or introducing a describeErrorCode() helper) and updating call sites that have these codes but no thrown error to use it; add tests covering those paths.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Fixed in 3dd68ccdescribeError now takes an optional errorCode parameter (see the MAX_DELIVERIES_EXCEEDED / REPLAY_TIMEOUT call sites in runtime.ts). Callers that already know the code pass it in; the internal branches match on that precomputed code, so they're no longer unreachable. Three new tests cover the paths.

Comment threadpackages/core/src/describe-error.ts Outdated
Comment on lines +32 to +71
const CONTEXT_ERROR_NAMES = new Set([
'NotInWorkflowContextError',
'NotInStepContextError',
'NotInWorkflowOrStepContextError',
'UnavailableInWorkflowContextError',
]);

/**
* Describe an error for user-facing presentation. Purely informational —
* does not change any persisted event data or error classification used by
* the runtime.
*
* The attribution here is more nuanced than `classifyRunError`:
*
* - `SerializationError` is technically raised by the SDK, but it almost
* always points at something the caller did (passed a non-serializable
* value, didn't register a class). We attribute it to the user.
* - Context-violation errors (`NotInWorkflowContextError`, etc.) likewise
* describe a user mistake.
* - `WorkflowRuntimeError` (and subclasses like `StepNotRegisteredError`)
* indicates an internal SDK invariant broke — surface that as `sdk`.
*/
export function describeError(err: unknown): ErrorDescription {
const errorCode = classifyRunError(err);
const name = err instanceof Error ? err.name : undefined;

if (SerializationError.is(err)) {
return {
attribution: 'user',
errorCode,
hint: 'A value passed across a workflow/step boundary could not be serialized. See the error message for the offending path and the Learn More link for details.',
};
}

if (name && CONTEXT_ERROR_NAMES.has(name)) {
return {
attribution: 'user',
errorCode,
hint: 'A workflow-only or step-only API was called from the wrong context. The error message includes the exact API and how to move the call.',
};

CopilotAIApr 23, 2026

Copy link

Choose a reason for hiding this comment

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

describeError() detects context-violation errors by comparing err.name against a string set. Since the concrete context error classes live in ./context-errors.ts, using instanceof checks (or a shared type guard) would be more robust and would fail at compile-time if those classes are renamed, instead of silently losing attribution/hints.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Fixed in 3dd68ccdescribeError now detects context-violation errors with instanceof against the concrete classes (NotInWorkflowContextError et al.) rather than a string Set of names, so renames can't silently break the classification.

Comment on lines 578 to 595
const errorCode = classifyRunError(err);
const description = describeError(err);
const framing =
description.attribution === 'sdk'
? `Workflow "${workflowName}" failed due to an SDK runtime error`
: `Workflow "${workflowName}" threw`;

// Use the stack as the primary message so it shows up
// in flattened logs without structured metadata.
runLogger.error(
errorStack || 'Unknown error encountered in workflow',
`${framing}\n${errorStack || 'Unknown error encountered in workflow'}`,
{
errorCode,
errorAttribution: description.attribution,
errorName,
errorMessage,
...(description.hint ? { hint: description.hint } : {}),
}

CopilotAIApr 23, 2026

Copy link

Choose a reason for hiding this comment

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

In this failure path errorCode is computed via classifyRunError(err) and describeError(err) recomputes it internally. Consider passing the already-computed errorCode into describeError (or returning attribution/hint separately) to avoid duplicated work and guarantee the logged errorCode and the description stay in sync if the classification logic evolves.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Addressed in 3dd68cc — the call site in runtime.ts now passes the precomputed errorCode into describeError(err, errorCode), so classifyRunError is only called once.

@@ -0,0 +1,11 @@
---
'@workflow/core': patch

CopilotAIApr 23, 2026

Copy link

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 consistently use double quotes (YAML string). For consistency (and to match existing tooling expectations), switch this to "@workflow/core": patch.

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

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

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

Fixed in 3dd68cc — single-quoted frontmatter switched to double-quoted.

@pranaygp

Copy link
Copy Markdown
ContributorAuthor

Stacked: #1838 — Phase 6, cosmetic consistency pass on remaining bare throws

pranaygpand others added 2 commits April 23, 2026 18:15
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.
@pranaygp
pranaygpforce-pushed the pranaygp/friendlier-errors-phase-5-attribution branch from eb171bb to 3dd68ccCompareApril 24, 2026 01:18
@changeset-bot

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 3dd68cc

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

This PR includes changesets to release 17 packages
NameType
@workflow/corePatch
@workflow/buildersPatch
@workflow/cliPatch
@workflow/nextPatch
@workflow/nitroPatch
@workflow/vitestPatch
@workflow/web-sharedPatch
@workflow/webPatch
workflowPatch
@workflow/world-testingPatch
@workflow/astroPatch
@workflow/nestPatch
@workflow/rollupPatch
@workflow/sveltekitPatch
@workflow/vitePatch
@workflow/nuxtPatch
@workflow/aiPatch

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

@pranaygp

Copy link
Copy Markdown
ContributorAuthor

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

Sign up for freeto 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

@pranaygp