Skip to content

Commit cb18ce3

Browse files
pranaygpclaude
andcommitted
feat: add semantic error types to replace HTTP status code checks in runtime
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 1f26e4e commit cb18ce3

12 files changed

Lines changed: 283 additions & 225 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
"@workflow/errors": patch
3+
"@workflow/core": patch
4+
"@workflow/world-local": patch
5+
"@workflow/world-vercel": patch
6+
"@workflow/world-postgres": patch
7+
---
8+
9+
Replace HTTP status code checks with semantic error types (EntityConflictError, RunExpiredError, ThrottleError)

packages/core/src/runtime.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
2+
EntityConflictError,
23
RUN_ERROR_CODES,
3-
WorkflowAPIError,
4+
RunExpiredError,
45
WorkflowRuntimeError,
56
} from '@workflow/errors';
67
import { classifyRunError } from './classify-error.js';
@@ -195,8 +196,8 @@ export function workflowEntrypoint(
195196
);
196197
} catch (failErr) {
197198
if (
198-
WorkflowAPIError.is(failErr) &&
199-
(failErr.status === 409 || failErr.status === 410)
199+
EntityConflictError.is(failErr) ||
200+
RunExpiredError.is(failErr)
200201
) {
201202
return;
202203
}
@@ -269,7 +270,7 @@ export function workflowEntrypoint(
269270
// Add the event to the events array so the workflow can see it
270271
events.push(result.event!);
271272
} catch (err) {
272-
if (WorkflowAPIError.is(err) && err.status === 409) {
273+
if (EntityConflictError.is(err)) {
273274
runtimeLogger.info('Wait already completed, skipping', {
274275
workflowRunId: runId,
275276
correlationId: waitEvent.correlationId,
@@ -393,8 +394,8 @@ export function workflowEntrypoint(
393394
);
394395
} catch (failErr) {
395396
if (
396-
WorkflowAPIError.is(failErr) &&
397-
(failErr.status === 409 || failErr.status === 410)
397+
EntityConflictError.is(failErr) ||
398+
RunExpiredError.is(failErr)
398399
) {
399400
runtimeLogger.info(
400401
'Tried failing workflow run, but run has already finished.',
@@ -440,8 +441,8 @@ export function workflowEntrypoint(
440441
);
441442
} catch (err) {
442443
if (
443-
WorkflowAPIError.is(err) &&
444-
(err.status === 409 || err.status === 410)
444+
EntityConflictError.is(err) ||
445+
RunExpiredError.is(err)
445446
) {
446447
runtimeLogger.info(
447448
'Tried completing workflow run, but run has already finished.',

packages/core/src/runtime/runs.test.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { WorkflowAPIError, WorkflowRunNotFoundError } from '@workflow/errors';
1+
import { EntityConflictError, WorkflowAPIError, WorkflowRunNotFoundError } from '@workflow/errors';
22
import type { Event, World } from '@workflow/world';
33
import { afterEach, describe, expect, it, vi } from 'vitest';
44

@@ -64,9 +64,7 @@ describe('wakeUpRun', () => {
6464
},
6565
];
6666

67-
const conflict = new WorkflowAPIError('Wait already completed', {
68-
status: 409,
69-
});
67+
const conflict = new EntityConflictError('Wait already completed');
7068

7169
const world = createMockWorld({ events, createError: conflict });
7270
const result = await wakeUpRun(world, 'wrun_123');

packages/core/src/runtime/runs.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { WorkflowAPIError } from '@workflow/errors';
1+
import { EntityConflictError } from '@workflow/errors';
22
import {
33
type Event,
44
isLegacySpecVersion,
@@ -192,7 +192,7 @@ export async function wakeUpRun(
192192
await world.events.create(runId, eventData, { v1Compat: compatMode });
193193
stoppedCount++;
194194
} catch (err) {
195-
if (WorkflowAPIError.is(err) && err.status === 409) {
195+
if (EntityConflictError.is(err)) {
196196
stoppedCount++;
197197
} else {
198198
errors.push(err instanceof Error ? err : new Error(String(err)));

packages/core/src/runtime/step-handler.test.ts

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { WorkflowAPIError } from '@workflow/errors';
1+
import { EntityConflictError, WorkflowAPIError } from '@workflow/errors';
22
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
33

44
// Use vi.hoisted so these are available in mock factories
@@ -248,9 +248,8 @@ describe('step-handler 409 handling', () => {
248248
if (event.eventType === 'step_completed') {
249249
callCount++;
250250
return Promise.reject(
251-
new WorkflowAPIError(
252-
'Cannot complete step because it is already completed',
253-
{ status: 409 }
251+
new EntityConflictError(
252+
'Cannot complete step because it is already completed'
254253
)
255254
);
256255
}
@@ -300,9 +299,8 @@ describe('step-handler 409 handling', () => {
300299
}
301300
if (event.eventType === 'step_failed') {
302301
return Promise.reject(
303-
new WorkflowAPIError(
304-
'Cannot fail step because it is already completed',
305-
{ status: 409 }
302+
new EntityConflictError(
303+
'Cannot fail step because it is already completed'
306304
)
307305
);
308306
}
@@ -347,9 +345,8 @@ describe('step-handler 409 handling', () => {
347345
}
348346
if (event.eventType === 'step_failed') {
349347
return Promise.reject(
350-
new WorkflowAPIError(
351-
'Cannot fail step because it is already completed',
352-
{ status: 409 }
348+
new EntityConflictError(
349+
'Cannot fail step because it is already completed'
353350
)
354351
);
355352
}
@@ -397,9 +394,8 @@ describe('step-handler 409 handling', () => {
397394
}
398395
if (event.eventType === 'step_retrying') {
399396
return Promise.reject(
400-
new WorkflowAPIError(
401-
'Cannot retry step because it is already completed',
402-
{ status: 409 }
397+
new EntityConflictError(
398+
'Cannot retry step because it is already completed'
403399
)
404400
);
405401
}

0 commit comments

Comments
 (0)