Skip to content

Commit 00b0bb9

Browse files
pranaygpCopilot
andauthored
Proper error stack propogating (#280)
* Proper stacktrace propogation in world Proper stacktrace propogation in world * Merge Reconciliation * Standardize the error type in the world spec * Deduplicate vercel world utils * fix undefined type issue --------- Co-authored-by: copilot-swe-agent[bot] <[email protected]>
1 parent 70429e4 commit 00b0bb9

File tree

34 files changed

+660
-187
lines changed

34 files changed

+660
-187
lines changed

.changeset/cruel-houses-sneeze.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@workflow/world-local": patch
3+
---
4+
5+
Support for structured errors
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@workflow/web-shared": patch
3+
---
4+
5+
Support structured error rendering

.changeset/good-icons-love.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@workflow/core": patch
3+
---
4+
5+
Implement the world's structured error interface

.changeset/postgres-error-stack.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@workflow/world-postgres": patch
3+
---
4+
5+
Support structured errors for steps and runs

.changeset/sour-tigers-serve.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@workflow/errors": patch
3+
---
4+
5+
Wire through world's structured errors in WorkflowRunFailedError

.changeset/witty-vans-report.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@workflow/world": patch
3+
---
4+
5+
Add error stack propogation to steps and runs
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@workflow/world-vercel": patch
3+
---
4+
5+
Support structured errors for steps and runs

CLAUDE.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,4 +160,5 @@ This project uses pnpm with workspace configuration. The required version is spe
160160
- All changed packages should be included in the changeset. Never include unchanged packages.
161161
- All changes should be marked as "patch". Never use "major" or "minor" modes.
162162
- Remember to always build any packages that get changed before running downstream tests like e2e tests in the workbench
163-
- Remember that changes made to one workbench should propogate to all other workbenches. The workflows should typically only be written once inside the example workbench and symlinked into all the other workbenches
163+
- Remember that changes made to one workbench should propogate to all other workbenches. The workflows should typically only be written once inside the example workbench and symlinked into all the other workbenches
164+
- When writing changeset, use the `pnpm changeset` command from the root of the repo. Keep the changesets terse (see existing changesets for examples). Try to amke chagnesets that are specific to each modified package so they are targeted. Ensure that any breaking changes are marked as "**BREAKING CHANGE**

packages/core/e2e/e2e.test.ts

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -561,13 +561,22 @@ describe('e2e', () => {
561561
const run = await triggerWorkflow('crossFileErrorWorkflow', []);
562562
const returnValue = await getWorkflowReturnValue(run.runId);
563563

564-
// The workflow should fail with the error from the helper module
565-
expect(returnValue).toHaveProperty('error');
566-
expect(returnValue.error).toContain('Error from imported helper module');
564+
// The workflow should fail with error response containing both top-level and cause
565+
expect(returnValue).toHaveProperty('name');
566+
expect(returnValue.name).toBe('WorkflowRunFailedError');
567+
expect(returnValue).toHaveProperty('message');
568+
569+
// Verify the cause property contains the structured error
570+
expect(returnValue).toHaveProperty('cause');
571+
expect(returnValue.cause).toBeTypeOf('object');
572+
expect(returnValue.cause).toHaveProperty('message');
573+
expect(returnValue.cause.message).toContain(
574+
'Error from imported helper module'
575+
);
567576

568-
// Verify the stack trace is present and shows correct file paths
569-
expect(returnValue).toHaveProperty('stack');
570-
expect(typeof returnValue.stack).toBe('string');
577+
// Verify the stack trace is present in the cause
578+
expect(returnValue.cause).toHaveProperty('stack');
579+
expect(typeof returnValue.cause.stack).toBe('string');
571580

572581
// Known issue: SvelteKit dev mode has incorrect source map mappings for bundled imports.
573582
// esbuild with bundle:true inlines helpers.ts but source maps incorrectly map to 99_e2e.ts
@@ -578,24 +587,27 @@ describe('e2e', () => {
578587

579588
if (!isSvelteKitDevMode) {
580589
// Stack trace should include frames from the helper module (helpers.ts)
581-
expect(returnValue.stack).toContain('helpers.ts');
590+
expect(returnValue.cause.stack).toContain('helpers.ts');
582591
}
583592

584593
// These checks should work in all modes
585-
expect(returnValue.stack).toContain('throwError');
586-
expect(returnValue.stack).toContain('callThrower');
594+
expect(returnValue.cause.stack).toContain('throwError');
595+
expect(returnValue.cause.stack).toContain('callThrower');
587596

588597
// Stack trace should include frames from the workflow file (99_e2e.ts)
589-
expect(returnValue.stack).toContain('99_e2e.ts');
590-
expect(returnValue.stack).toContain('crossFileErrorWorkflow');
598+
expect(returnValue.cause.stack).toContain('99_e2e.ts');
599+
expect(returnValue.cause.stack).toContain('crossFileErrorWorkflow');
591600

592601
// Stack trace should NOT contain 'evalmachine' anywhere
593-
expect(returnValue.stack).not.toContain('evalmachine');
602+
expect(returnValue.cause.stack).not.toContain('evalmachine');
594603

595-
// Verify the run failed
604+
// Verify the run failed with structured error
596605
const { json: runData } = await cliInspectJson(`runs ${run.runId}`);
597606
expect(runData.status).toBe('failed');
598-
expect(runData.error).toContain('Error from imported helper module');
607+
expect(runData.error).toBeTypeOf('object');
608+
expect(runData.error.message).toContain(
609+
'Error from imported helper module'
610+
);
599611
}
600612
);
601613
});

packages/core/src/runtime.ts

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import type {
1717
} from '@workflow/world';
1818
import { WorkflowSuspension } from './global.js';
1919
import { runtimeLogger } from './logger.js';
20+
import { parseWorkflowName } from './parse-name.js';
2021
import { getStepFunction } from './private.js';
2122
import { getWorld, getWorldHandlers } from './runtime/world.js';
2223
import {
@@ -33,6 +34,7 @@ import {
3334
hydrateStepArguments,
3435
hydrateWorkflowReturnValue,
3536
} from './serialization.js';
37+
import { remapErrorStack } from './source-map.js';
3638
// TODO: move step handler out to a separate file
3739
import { contextStorage } from './step/context-storage.js';
3840
import * as Attribute from './telemetry/semantic-conventions.js';
@@ -43,8 +45,6 @@ import {
4345
getWorkflowRunStreamId,
4446
} from './util.js';
4547
import { runWorkflow } from './workflow.js';
46-
import { remapErrorStack } from './source-map.js';
47-
import { parseWorkflowName } from './parse-name.js';
4848

4949
export type { Event, WorkflowRun };
5050
export { WorkflowSuspension } from './global.js';
@@ -205,10 +205,7 @@ export class Run<TResult> {
205205
}
206206

207207
if (run.status === 'failed') {
208-
throw new WorkflowRunFailedError(
209-
this.runId,
210-
run.error ?? 'Unknown error'
211-
);
208+
throw new WorkflowRunFailedError(this.runId, run.error);
212209
}
213210

214211
throw new WorkflowRunNotCompletedError(this.runId, run.status);
@@ -520,6 +517,8 @@ export function workflowEntrypoint(workflowCode: string) {
520517
}
521518
} else {
522519
const errorName = getErrorName(err);
520+
const errorMessage =
521+
err instanceof Error ? err.message : String(err);
523522
let errorStack = getErrorStack(err);
524523

525524
// Remap error stack using source maps to show original source locations
@@ -536,14 +535,13 @@ export function workflowEntrypoint(workflowCode: string) {
536535
console.error(
537536
`${errorName} while running "${runId}" workflow:\n\n${errorStack}`
538537
);
539-
540-
// Store both the error message and remapped stack trace
541-
const errorString = errorStack || String(err);
542-
543538
await world.runs.update(runId, {
544539
status: 'failed',
545-
error: errorString,
546-
// TODO: include error codes when we define them
540+
error: {
541+
message: errorMessage,
542+
stack: errorStack,
543+
// TODO: include error codes when we define them
544+
},
547545
});
548546
span?.setAttributes({
549547
...Attribute.WorkflowRunStatus('failed'),
@@ -739,7 +737,8 @@ export const stepEntrypoint =
739737
}
740738

741739
if (FatalError.is(err)) {
742-
const stackLines = getErrorStack(err).split('\n').slice(0, 4);
740+
const errorStack = getErrorStack(err);
741+
const stackLines = errorStack.split('\n').slice(0, 4);
743742
console.error(
744743
`[Workflows] "${workflowRunId}" - Encountered \`FatalError\` while executing step "${stepName}":\n > ${stackLines.join('\n > ')}\n\nBubbling up error to parent workflow`
745744
);
@@ -749,15 +748,17 @@ export const stepEntrypoint =
749748
correlationId: stepId,
750749
eventData: {
751750
error: String(err),
752-
stack: err.stack,
751+
stack: errorStack,
753752
fatal: true,
754753
},
755754
});
756755
await world.steps.update(workflowRunId, stepId, {
757756
status: 'failed',
758-
error: String(err),
759-
// TODO: include error codes when we define them
760-
// TODO: serialize/include the error name and stack?
757+
error: {
758+
message: err.message || String(err),
759+
stack: errorStack,
760+
// TODO: include error codes when we define them
761+
},
761762
});
762763

763764
span?.setAttributes({
@@ -774,7 +775,8 @@ export const stepEntrypoint =
774775

775776
if (attempt >= maxRetries) {
776777
// Max retries reached
777-
const stackLines = getErrorStack(err).split('\n').slice(0, 4);
778+
const errorStack = getErrorStack(err);
779+
const stackLines = errorStack.split('\n').slice(0, 4);
778780
console.error(
779781
`[Workflows] "${workflowRunId}" - Encountered \`Error\` while executing step "${stepName}" (attempt ${attempt}):\n > ${stackLines.join('\n > ')}\n\n Max retries reached\n Bubbling error to parent workflow`
780782
);
@@ -784,13 +786,16 @@ export const stepEntrypoint =
784786
correlationId: stepId,
785787
eventData: {
786788
error: errorMessage,
787-
stack: getErrorStack(err),
789+
stack: errorStack,
788790
fatal: true,
789791
},
790792
});
791793
await world.steps.update(workflowRunId, stepId, {
792794
status: 'failed',
793-
error: errorMessage,
795+
error: {
796+
message: errorMessage,
797+
stack: errorStack,
798+
},
794799
});
795800

796801
span?.setAttributes({

0 commit comments

Comments
 (0)