Skip to content
Merged
6 changes: 6 additions & 0 deletions .changeset/error-docs-and-exports.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"workflow": patch
"@workflow/errors": patch
---

Export semantic error types from `workflow/internal/errors` and add API reference documentation
2 changes: 1 addition & 1 deletion docs/content/docs/api-reference/meta.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{
"title": "API Reference",
"pages": ["...", "workflow-serde", "workflow-ai", "vitest"]
"pages": ["...", "workflow-errors", "workflow-serde", "workflow-ai", "vitest"]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
---
title: EntityConflictError
description: Thrown when a storage operation conflicts with the current entity state.
type: reference
summary: Catch EntityConflictError when a world operation conflicts with entity state (e.g. duplicate events or runs).
related:
- /docs/api-reference/workflow-errors/workflow-world-error
- /docs/api-reference/workflow-errors/run-expired-error
---

`EntityConflictError` is thrown by world implementations when a storage operation conflicts with the current entity state. This includes cases like creating a run that already exists or writing an event that has already been persisted.

It corresponds to HTTP 409 Conflict semantics.

<Callout>
The Workflow runtime handles this error automatically during replay and event deduplication. You will only encounter it when interacting with world storage APIs directly.
</Callout>

```typescript lineNumbers
import { EntityConflictError } from "workflow/errors"
declare const world: { events: { create(...args: any[]): Promise<any> } }; // @setup
declare const runId: string; // @setup
declare const event: any; // @setup

try {
await world.events.create(runId, event);
} catch (error) {
if (EntityConflictError.is(error)) { // [!code highlight]
// Event already exists — safe to ignore during replay
}
}
```

## API Signature

### Properties

<TSDoc
definition={`
interface EntityConflictError {
/** The error message. */
message: string;
}
export default EntityConflictError;`}
/>

### Static Methods

#### `EntityConflictError.is(value)`

Type-safe check for `EntityConflictError` instances. Preferred over `instanceof` because it works across module boundaries and VM contexts.

```typescript
import { EntityConflictError } from "workflow/errors"
declare const error: unknown; // @setup

if (EntityConflictError.is(error)) {
// error is typed as EntityConflictError
}
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
---
title: HookNotFoundError
description: Thrown when resuming a hook that does not exist.
type: reference
summary: Catch HookNotFoundError when calling resumeHook() or resumeWebhook() with a token that doesn't match any active hook.
related:
- /docs/api-reference/workflow/create-hook
- /docs/api-reference/workflow/define-hook
---

`HookNotFoundError` is thrown when calling `resumeHook()` or `resumeWebhook()` with a token that does not match any active hook. This typically happens when:

- The hook has expired (past its TTL)
- The hook was already consumed and disposed
- The workflow has not started yet, so the hook does not exist

```typescript lineNumbers
import { HookNotFoundError } from "workflow/errors"
declare function resumeHook(token: string, payload: any): Promise<any>; // @setup
declare const token: string; // @setup
declare const payload: any; // @setup

try {
await resumeHook(token, payload);
} catch (error) {
if (HookNotFoundError.is(error)) { // [!code highlight]
console.error("Hook not found:", error.token);
}
}
```

## API Signature

### Properties

<TSDoc
definition={`
interface HookNotFoundError {
/** The hook token that was not found. */
token: string;
/** The error message. */
message: string;
}
export default HookNotFoundError;`}
/>

### Static Methods

#### `HookNotFoundError.is(value)`

Type-safe check for `HookNotFoundError` instances. Preferred over `instanceof` because it works across module boundaries and VM contexts.

```typescript
import { HookNotFoundError } from "workflow/errors"
declare const error: unknown; // @setup

if (HookNotFoundError.is(error)) {
// error is typed as HookNotFoundError
}
```

## Examples

### Resume hook or start workflow

A common pattern for idempotent workflows is to try resuming a hook, and if it doesn't exist yet, start a new workflow run with the input data.

<Callout>
This "resume or start" pattern is not atomic — there is a small window where a race condition is possible. A better native approach is being worked on, but this pattern works well for many use cases.
</Callout>

```typescript lineNumbers
import { HookNotFoundError } from "workflow/errors"
declare function resumeHook(token: string, data: unknown): Promise<any>; // @setup
declare function startWorkflow(name: string, data: unknown): Promise<any>; // @setup

async function handleIncomingEvent(token: string, data: unknown) {
try {
// Try to resume an existing hook
await resumeHook(token, data);
} catch (error) {
if (HookNotFoundError.is(error)) { // [!code highlight]
// Hook doesn't exist yet — start a new workflow run
await startWorkflow("processEvent", data); // [!code highlight]
} else {
throw error;
}
}
}
```
14 changes: 14 additions & 0 deletions docs/content/docs/api-reference/workflow-errors/meta.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"title": "workflow/errors",
"pages": [
"hook-not-found-error",
"workflow-run-not-found-error",
"workflow-run-failed-error",
"workflow-run-cancelled-error",
"workflow-world-error",
"throttle-error",
"entity-conflict-error",
"run-expired-error",
"too-early-error"
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
---
title: RunExpiredError
description: Thrown when a workflow run has expired and can no longer be operated on.
type: reference
summary: Catch RunExpiredError when a workflow run has expired and can no longer accept operations.
related:
- /docs/api-reference/workflow-errors/workflow-world-error
- /docs/api-reference/workflow-errors/entity-conflict-error
---

`RunExpiredError` is thrown by world implementations when a workflow run has expired and can no longer be operated on. It corresponds to HTTP 410 Gone semantics.

<Callout>
The Workflow runtime handles this error automatically. You will only encounter it when interacting with world storage APIs directly.
</Callout>

```typescript lineNumbers
import { RunExpiredError } from "workflow/errors"
declare const world: { events: { create(...args: any[]): Promise<any> } }; // @setup
declare const runId: string; // @setup
declare const event: any; // @setup

try {
await world.events.create(runId, event);
} catch (error) {
if (RunExpiredError.is(error)) { // [!code highlight]
console.log("Run has expired and can no longer accept events");
}
}
```

## API Signature

### Properties

<TSDoc
definition={`
interface RunExpiredError {
/** The error message. */
message: string;
}
export default RunExpiredError;`}
/>

### Static Methods

#### `RunExpiredError.is(value)`

Type-safe check for `RunExpiredError` instances. Preferred over `instanceof` because it works across module boundaries and VM contexts.

```typescript
import { RunExpiredError } from "workflow/errors"
declare const error: unknown; // @setup

if (RunExpiredError.is(error)) {
// error is typed as RunExpiredError
}
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
title: ThrottleError
description: Thrown when a request is rate-limited by the workflow backend.
type: reference
summary: Catch ThrottleError when a workflow storage operation is rate-limited (HTTP 429).
related:
- /docs/api-reference/workflow-errors/workflow-world-error
- /docs/api-reference/workflow-errors/too-early-error
---

`ThrottleError` is thrown when a request to the workflow backend is rate-limited. It corresponds to HTTP 429 Too Many Requests semantics.

The `retryAfter` property contains the number of seconds to wait before retrying.

<Callout>
The Workflow runtime handles this error automatically by backing off and retrying. You will only encounter it when interacting with world storage APIs directly.
</Callout>

```typescript lineNumbers
import { ThrottleError } from "workflow/errors"
declare const world: { events: { create(...args: any[]): Promise<any> } }; // @setup
declare const runId: string; // @setup
declare const event: any; // @setup

try {
await world.events.create(runId, event);
} catch (error) {
if (ThrottleError.is(error)) { // [!code highlight]
console.log(`Rate limited. Retry after ${error.retryAfter} seconds`);
}
}
```

## API Signature

### Properties

<TSDoc
definition={`
interface ThrottleError {
/** The number of seconds to wait before retrying. Present when the server sends a Retry-After header. */
retryAfter?: number;
/** The error message. */
message: string;
}
export default ThrottleError;`}
/>

### Static Methods

#### `ThrottleError.is(value)`

Type-safe check for `ThrottleError` instances. Preferred over `instanceof` because it works across module boundaries and VM contexts.

```typescript
import { ThrottleError } from "workflow/errors"
declare const error: unknown; // @setup

if (ThrottleError.is(error)) {
// error is typed as ThrottleError
}
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
---
title: TooEarlyError
description: Thrown when a request is made before the system is ready to process it.
type: reference
summary: Catch TooEarlyError when a world operation is attempted before the system is ready.
related:
- /docs/api-reference/workflow-errors/workflow-world-error
- /docs/api-reference/workflow-errors/throttle-error
---

`TooEarlyError` is thrown by world implementations when a request is made before the system is ready to process it. It corresponds to HTTP 425 Too Early semantics.

The `retryAfter` property contains the number of seconds to wait before retrying.

<Callout>
The Workflow runtime handles this error automatically by retrying after the specified delay. You will only encounter it when interacting with world storage APIs directly.
</Callout>

```typescript lineNumbers
import { TooEarlyError } from "workflow/errors"
declare const world: { events: { create(...args: any[]): Promise<any> } }; // @setup
declare const runId: string; // @setup
declare const event: any; // @setup

try {
await world.events.create(runId, event);
} catch (error) {
if (TooEarlyError.is(error)) { // [!code highlight]
console.log(`Retry after ${error.retryAfter} seconds`);
}
}
```

## API Signature

### Properties

<TSDoc
definition={`
interface TooEarlyError {
/** Delay in seconds before the operation can be retried. Present when the server sends a Retry-After header. */
retryAfter?: number;
/** The error message. */
message: string;
}
export default TooEarlyError;`}
/>

### Static Methods

#### `TooEarlyError.is(value)`

Type-safe check for `TooEarlyError` instances. Preferred over `instanceof` because it works across module boundaries and VM contexts.

```typescript
import { TooEarlyError } from "workflow/errors"
declare const error: unknown; // @setup

if (TooEarlyError.is(error)) {
// error is typed as TooEarlyError
}
```
Loading
Loading