Skip to content

Commit 45cbc3b

Browse files
committed
Standardize the error type in the world spec
1 parent 5beca82 commit 45cbc3b

File tree

32 files changed

+657
-237
lines changed

32 files changed

+657
-237
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

.changeset/fix-error-stack-rendering.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
"@workflow/web-shared": patch
33
---
44

5-
Fix error and errorStack rendering to display multiline text correctly
5+
Support structured error rendering

.changeset/good-icons-love.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
"@workflow/core": patch
33
---
44

5-
Propogate error stack for runs using the new world stack proeprty
5+
Implement the world's structured error interface

.changeset/postgres-error-stack.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
"@workflow/world-postgres": patch
33
---
44

5-
Add errorStack column to workflow runs and steps tables
5+
Support structured errors for steps and runs

.changeset/sour-tigers-serve.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
"@workflow/errors": patch
33
---
44

5-
Add stack to WorkflowRunFailedError
5+
Wire through world's structured errors in WorkflowRunFailedError

.changeset/world-vercel-error-serialization.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
"@workflow/world-vercel": patch
33
---
44

5-
Serialize error and stack as JSON in error field for Vercel backend
5+
Support structured errors for steps and runs

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: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -205,11 +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-
run.errorStack
212-
);
208+
throw new WorkflowRunFailedError(this.runId, run.error);
213209
}
214210

215211
throw new WorkflowRunNotCompletedError(this.runId, run.status);
@@ -541,9 +537,11 @@ export function workflowEntrypoint(workflowCode: string) {
541537
);
542538
await world.runs.update(runId, {
543539
status: 'failed',
544-
error: errorMessage,
545-
errorStack: errorStack,
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'),
@@ -756,9 +754,11 @@ export const stepEntrypoint =
756754
});
757755
await world.steps.update(workflowRunId, stepId, {
758756
status: 'failed',
759-
error: err.message || String(err),
760-
errorStack,
761-
// TODO: include error codes when we define them
757+
error: {
758+
message: err.message || String(err),
759+
stack: errorStack,
760+
// TODO: include error codes when we define them
761+
},
762762
});
763763

764764
span?.setAttributes({
@@ -792,8 +792,10 @@ export const stepEntrypoint =
792792
});
793793
await world.steps.update(workflowRunId, stepId, {
794794
status: 'failed',
795-
error: errorMessage,
796-
errorStack: errorStack,
795+
error: {
796+
message: errorMessage,
797+
stack: errorStack,
798+
},
797799
});
798800

799801
span?.setAttributes({

packages/errors/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@
3131
"devDependencies": {
3232
"@types/ms": "^2.1.0",
3333
"@types/node": "catalog:",
34-
"@workflow/tsconfig": "workspace:*"
34+
"@workflow/tsconfig": "workspace:*",
35+
"@workflow/world": "workspace:*"
3536
},
3637
"dependencies": {
3738
"@workflow/utils": "workspace:*",

packages/errors/src/index.ts

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { parseDurationToDate } from '@workflow/utils';
2+
import type { StructuredError } from '@workflow/world';
23
import type { StringValue } from 'ms';
34

45
const BASE_URL = 'https://useworkflow.dev/err';
@@ -121,8 +122,8 @@ export class WorkflowAPIError extends WorkflowError {
121122
* Thrown when a workflow run fails during execution.
122123
*
123124
* This error indicates that the workflow encountered a fatal error
124-
* and cannot continue. The `error` property contains details about
125-
* what caused the failure.
125+
* and cannot continue. The `cause` property contains the underlying
126+
* error with its message, stack trace, and optional error code.
126127
*
127128
* @example
128129
* ```
@@ -134,18 +135,24 @@ export class WorkflowAPIError extends WorkflowError {
134135
*/
135136
export class WorkflowRunFailedError extends WorkflowError {
136137
runId: string;
137-
error: string;
138-
declare stack: string;
138+
declare cause: Error & { code?: string };
139+
140+
constructor(runId: string, error: StructuredError) {
141+
// Create a proper Error instance from the StructuredError to set as cause
142+
// NOTE: custom error types do not get serialized/deserialized. Everything is an Error
143+
const causeError = new Error(error.message);
144+
if (error.stack) {
145+
causeError.stack = error.stack;
146+
}
147+
if (error.code) {
148+
(causeError as any).code = error.code;
149+
}
139150

140-
constructor(runId: string, error: string, errorStack?: string) {
141-
super(`Workflow run "${runId}" failed: ${error}`, {});
151+
super(`Workflow run "${runId}" failed: ${error.message}`, {
152+
cause: causeError,
153+
});
142154
this.name = 'WorkflowRunFailedError';
143155
this.runId = runId;
144-
this.error = error;
145-
// Override the stack with the workflow's error stack if available
146-
if (errorStack !== undefined) {
147-
this.stack = errorStack;
148-
}
149156
}
150157

151158
static is(value: unknown): value is WorkflowRunFailedError {

0 commit comments

Comments
 (0)