﻿# FlowMarkup Standard Error Catalog

**Version:** 0.9.0
**Date:** 2026-03-28
**Designed by:** Łukasz Nawojczyk
**Copyright:** © 2026 Progralink Łukasz Nawojczyk. All rights reserved.
**License:** Open Web Foundation Agreement 1.0 (OWFa 1.0)

## 1. Overview

FlowMarkup defines the built-in error types listed in the hierarchy below (plus the test-only types in section 4) that engines MUST implement. These errors form a single-inheritance hierarchy rooted at the abstract `FlowError` base.

### Error Naming

All error types use **PascalCase** with the suffix **`Error`** (e.g., `TimeoutError`, `PaymentDeclinedError`). User-defined error types follow the same convention.

### Inheritance

System errors support single inheritance via `$parent:` in `throws:` declarations:

```yaml
throws:
- $kind: PaymentDeclinedError
  $parent: ValidationError
  data:
    amount: NUMBER
    reason: TEXT
```

System error types are implicitly available as parents -- flows do not need to re-declare them. `catch:` is polymorphic: catching a parent type also catches all descendants.

### The `ERROR` Binding

Inside `catch:` blocks and error handlers, the `ERROR` binding provides:

| Field | Type | Description |
|---|---|---|
| `ERROR.TYPE` | STRING | Exact error type name |
| `ERROR.MESSAGE` | STRING | Human-readable error message |
| `ERROR.STEP` | STRING | `_id_` of the step that threw (if set) |
| `ERROR.DATA` | MAP | Type-specific structured fields (see per-error tables below) |
| `ERROR.CAUSE` | ERROR | Wrapped original error (recursive; has TYPE, MESSAGE, STEP, DATA, CAUSE) |

> **ERROR.CAUSE chain limits.** The ERROR.CAUSE chain MUST be limited to a configurable maximum depth (default: 16, engine-configurable up to a maximum of 64). When the limit is reached, the engine MUST truncate the chain at the tail and replace the deepest entry with a synthetic error: `{TYPE: "TruncatedCauseError", MESSAGE: "Error cause chain truncated — N additional causes omitted", DATA: null, CAUSE: null}`. The original deepest causes are discarded from the catch-handler-visible chain (they MUST still be preserved in the engine's internal audit log).
>
> **Recursive redaction.** When flattening or serializing error chains, engines MUST apply secret redaction recursively to every `ERROR.DATA` at every level of the chain. This includes all STRING-typed fields within DATA maps at every nesting depth. The redaction pass MUST use the same secret-value matching as log output redaction (per ENGINE.md item 51), replacing every occurrence of every resolved secret value in the current execution scope with `[REDACTED]`. The redaction MUST be applied before the ERROR binding is made available to `catch:` handlers, `finally:` blocks, or any CEL expression that accesses `ERROR`. *(CWE-209)*

### Retryability

- **Retryable:** Transient infrastructure errors (timeout, connection, rate limit, service unavailable)
- **Non-retryable:** Permanent errors (validation, auth, access, config, business logic)

> **Permanent error types** (canonical list): `ValidationError`, `BadRequestError`, `AuthenticationError`, `AccessDeniedError`, `MissingCapabilityError`, `NotFoundError`, `ConfigurationError`, `AssertionError`, `AddressError`. These errors indicate structural or input-dependent faults that will not resolve on retry with the same input. SA rules referencing "permanent error types" (SA-RETRY-2, SA-CB-4, SA-DEF-2) MUST use this list.
- Controlled via `retry.onErrors:` (whitelist) or `retry.nonRetryable:` (blacklist) -- mutually exclusive

### Execution Order

```
circuitBreaker -> rateLimit -> retry -> timeout -> action execution
  -> rollback (on error) -> catch -> finally
```

---

**Notation:** `SS` = FLOWMARKUP-SPECIFICATION.md Section (e.g., `SS4.6` = FLOWMARKUP-SPECIFICATION.md Section 4.6). Sections 5-7 are maintained in FLOWMARKUP-ENGINE.md per the §5 stub in SPECIFICATION.md.

## 2. Error Hierarchy

```
FlowError (abstract base)
|
+-- Transient (retryable)
|   +-- TimeoutError
|   +-- ConnectionError
|   +-- TLSError
|   +-- RateLimitError
|   +-- EventBufferFullError
|   +-- ServiceUnavailableError
|   +-- ServiceError
|
+-- Client / Input (non-retryable)
|   +-- ValidationError
|   +-- BadRequestError
|   +-- AuthenticationError
|   +-- AccessDeniedError
|   +-- MissingCapabilityError
|   +-- NotFoundError
|
+-- Configuration (non-retryable)
|   +-- ConfigurationError
|
+-- Execution (non-retryable)
|   +-- AssertionError
|   +-- CancellationError
|   +-- StackOverflowError
|   +-- DuplicateInvocationError
|   +-- CircuitOpenError
|   +-- ConflictError
|   +-- DeadlockError
|   +-- GroupError
|
+-- Rollback (non-retryable)
|   +-- RolledBackError
|   +-- RollbackFailedError
|
+-- Action-specific
|   +-- HttpError (request)
|   +-- ExecError (exec)
|   +-- MailError (mail)
|   |   +-- SmtpError
|   |   +-- AddressError
|   +-- StorageError (storage)
|   |   +-- StoragePathError
|   +-- SshError (ssh)
|
+-- Data / Encoding (non-retryable)
|   +-- ParseError
|   +-- EncodeError
|   +-- XPathError
|
+-- Resource limits (non-retryable)
|   +-- ResourceExhaustedError
|   +-- ResourceLimitError
|   +-- ResourceNotFoundError
|
+-- Secrets (non-retryable)
|   +-- SecretAccessError
|   +-- SecretTypeError
|   +-- SecretRateLimitError
|   +-- SecretInjectionError
|   +-- SecretConfigurationError
|
+-- Non-catchable (engine-level)
    +-- EngineError
    +-- FlowVersionError
    +-- FlowFetchError
    +-- MigrationError
    +-- SecurityViolationError
    +-- TruncatedCauseError
    +-- VersionParseError
    +-- SchemaLoadError
    +-- SchemaResolutionError
    +-- UnsupportedProviderError
```

> **Note:** Category labels (Transient, Client/Input, Action-Specific, etc.) are organizational groupings for documentation purposes. They are not catchable error types. To catch all errors in a category, list each concrete type individually in the `catch:` map.

---

## 3. Error Reference

### 3.1 Transient Errors (Retryable)

These errors indicate temporary conditions that may succeed on retry.

---

#### TimeoutError

| Property | Value |
|---|---|
| **Description** | Action or directive exceeded configured `timeout:` duration |
| **Retryable** | Yes |
| **Catchable** | Yes |
| **Producing steps** | Any action with `timeout:`, `lock:` (acquisition timeout), `waitFor:`, `waitUntil:`, `group:` |
| **Spec reference** | SS4.1 |

**ERROR.DATA:** None (timeout duration available from step configuration).

---

#### ConnectionError

| Property | Value |
|---|---|
| **Description** | Network connectivity failure, DNS resolution failure, SSRF check failure, or redirect to unallowed origin |
| **Retryable** | Yes |
| **Catchable** | Yes |
| **Producing steps** | `request:` action, `storage:` action, `ssh:` action |
| **Spec reference** | SS4.1, SS4.6 |

**ERROR.DATA:** None.

---

#### TLSError

| Property | Value |
|---|---|
| **Description** | TLS/SSL handshake failure or certificate validation error |
| **Retryable** | Yes |
| **Catchable** | Yes |
| **Producing steps** | `request:` action (HTTPS), `storage:` action (FTPS, HTTPS S3 endpoints), `ssh:` action (SSH host key verification failure) |
| **Spec reference** | SS4.1, SS4.6 |

**ERROR.DATA:** None.

---

#### RateLimitError

| Property | Value |
|---|---|
| **Description** | Rate limit quota exceeded; thrown when strategy is `REJECT`, or when `WAIT` strategy's `rateLimit.timeout` elapses |
| **Retryable** | Yes |
| **Catchable** | Yes |
| **Producing steps** | `rateLimit:` policy on any action, `request:` (HTTP 429) |
| **Spec reference** | SS4.1, SS4.2 |

**ERROR.DATA:** None.

**Timeout precedence:** The `WAIT` strategy uses `rateLimit.timeout` exclusively for wait-queue timeout (raises `RateLimitError`). The step-level `timeout:` applies to the entire step execution including any rate-limit wait (raises `TimeoutError`). If both fire simultaneously, `TimeoutError` takes precedence.

---

#### EventBufferFullError

| Property | Value |
|---|---|
| **Description** | Per-flow-instance event quota exceeded (default: 256 per scope) or per-scope buffer full (default: 1024) |
| **Retryable** | Yes |
| **Catchable** | Yes |
| **Producing steps** | `emit:` directive |
| **Spec reference** | SS3.19, SS4.1 |

**ERROR.DATA:**

| Field | Type | Description |
|---|---|---|
| `scope` | STRING | LOCAL, CONTEXT, or GLOBAL |
| `buffer_size` | NUMBER | Current buffer capacity |
| `emitted_count` | NUMBER | This flow's emit count |

---

#### ServiceUnavailableError

| Property | Value |
|---|---|
| **Description** | Service temporarily unavailable (HTTP 5xx equivalent) |
| **Retryable** | Yes |
| **Catchable** | Yes |
| **Producing steps** | `call:` action, inline `SERVICES.<alias>.<op>()` |
| **Spec reference** | SS4.1, SS4.6 |

**ERROR.DATA:** None.

---

#### ServiceError

| Property | Value |
|---|---|
| **Description** | Generic transient service execution error from the provider |
| **Retryable** | Yes |
| **Catchable** | Yes |
| **Producing steps** | `call:` action, inline `SERVICES.<alias>.<op>()` |
| **Spec reference** | SS4.1, SS4.6 |

**ERROR.DATA:**

| Field | Type | Description |
|---|---|---|
| `service` | STRING | Service alias name |
| `operation` | STRING | Operation name |
| `cause` | STRING | Original exception message |

> **Security:** `ServiceError.DATA.cause` from external service responses MUST be sanitized before exposure to catch handlers. The engine MUST:
> - Remove Java/Python/.NET exception class names that reveal implementation technology
> - Remove stack traces entirely
> - Truncate the cause message to a maximum of 500 characters (reduced from 512 to align with defense-in-depth truncation standards)
> - Apply standard secret redaction to the cause message — every occurrence of every resolved secret value in the current execution scope MUST be replaced with `[REDACTED]`
> - Redact any content matching known secret patterns (per ENGINE.md item 51), including but not limited to: API keys, bearer tokens, connection strings, base64-encoded credentials, and AWS-style access keys
>
> This prevents service implementation details from leaking through error chains. The full, unredacted error MUST still be written to the engine's internal audit log for debugging. *(CWE-209)*
>
> **Recursive cause sanitization:** Technology-identifier stripping and truncation MUST apply recursively at every `ERROR.CAUSE` level, not only the top-level `ServiceError`. Each level's `DATA.cause` MUST be independently truncated to 500 characters and stripped of stack traces and technology identifiers.

---

### 3.2 Client / Input Errors (Non-retryable)

These errors indicate caller mistakes. Retrying with the same input will produce the same error.

---

#### ValidationError

| Property | Value |
|---|---|
| **Description** | Input validation failed: format regex mismatch, type mismatch, null on non-nullable, or auto-decode failure |
| **Retryable** | No |
| **Catchable** | Yes |
| **Producing steps** | `input:` contract validation, `set:` (type/format checks), `params:` on any action, `request: parseAs:` auto-decode, `exec: parseAs:` auto-decode |
| **Spec reference** | SS1.3, SS2.4, SS4.1 |

**ERROR.DATA:**

| Field | Type | Description |
|---|---|---|
| `field` | STRING | Data element or param name that failed |
| `pattern` | STRING | Expected regex (if `$format:` set) |
| `expected_type` | STRING | Expected `$kind:` (if type mismatch) |
| `reason` | STRING | Human-readable failure reason |

> **Security:** When `ValidationError.DATA` includes a `pattern` field (e.g., the regular expression that failed to match), engines MUST truncate the pattern to a maximum of 128 characters and MUST strip any embedded comments or flags that could reveal validation logic. This prevents information leakage that could aid input-crafting attacks. *(CWE-209)*
>
> **Dynamic pattern sanitization:** When `ERROR.DATA.pattern` contains a regex derived from user input or CEL expressions (i.e., the `$format:` value was computed at runtime rather than declared as a static literal), engines MUST truncate it to a maximum of 200 characters to prevent regex content from leaking internal validation logic. The truncated value MUST end with `…[truncated]` to indicate the pattern was shortened. This limit applies in addition to the 128-character limit for static patterns — dynamic patterns are permitted the higher limit because they may include user-supplied content that aids debugging, but MUST NOT exceed 200 characters. *(CWE-209)*

---

#### BadRequestError

| Property | Value |
|---|---|
| **Description** | Malformed action input detected by service (HTTP 400 equivalent) |
| **Retryable** | No |
| **Catchable** | Yes |
| **Producing steps** | `request:` action (HTTP 4xx), `call:` action |
| **Spec reference** | SS4.1 |

**ERROR.DATA:** None.

---

#### AuthenticationError

| Property | Value |
|---|---|
| **Description** | Invalid or expired credentials (HTTP 401 equivalent) |
| **Retryable** | No |
| **Catchable** | Yes |
| **Producing steps** | `request:` action (HTTP 401), `call:` action, `mail:` action (SMTP auth), `storage:` action (credential failure), `ssh:` action (SSH auth failure) |
| **Spec reference** | SS4.1 |

**ERROR.DATA:** None.

---

#### AccessDeniedError

| Property | Value |
|---|---|
| **Description** | Authenticated but lacks permission (HTTP 403 equivalent) |
| **Retryable** | No |
| **Catchable** | Yes |
| **Producing steps** | `request:` action (HTTP 403), `call:` action, `storage:` action (remote permission denied), `ssh:` action (remote permission denied) |
| **Spec reference** | SS4.1 |

**ERROR.DATA:** None.

---

#### MissingCapabilityError

| Property | Value |
|---|---|
| **Description** | Flow or action requires a capability not granted by caller or `cap:` |
| **Retryable** | No |
| **Catchable** | Yes |
| **Producing steps** | `run:` (sub-flow capability check), `request:` (REQUEST check), `exec:` (EXEC check), `mail:` (MAIL check), `storage:` (STORAGE check), `ssh:` (SSH check), `SECRET.*` access, `GLOBAL.*`/`CONTEXT.*` per-key access |
| **Spec reference** | SS2.5, SS2.8, SS5.3 |

**ERROR.DATA:**

| Field | Type | Description |
|---|---|---|
| `capability` | STRING | Category name (SECRET, EXEC, REQUEST, etc.) |
| `required` | ANY | What the flow declared it needs |
| `granted` | ANY | What the caller provided |
| `missing` | ANY | Gap between required and granted |

> **Security:** The `ERROR.DATA.granted` field MUST be omitted from `MissingCapabilityError` when the error is propagated to the calling flow (as opposed to being logged internally). Exposing the full set of granted capabilities allows an attacker to enumerate the caller's permission boundary and craft targeted privilege escalation attempts. Only `ERROR.DATA.missing` (the specific capability that was denied) and `ERROR.DATA.capability` (the category) SHOULD be exposed to catch handlers. The full capability set MUST be written to the audit log. *(CWE-200)*
>
> **Capability exposure limits:** The error MUST report only the *category* of the missing capability (e.g., `"STORAGE"`, `"SSH"`, `"REQUEST"`, `"EXEC"`) via `ERROR.DATA.capability` and the *operation* attempted (e.g., `"get"`, `"put"`, `"connect"`) via `ERROR.DATA.missing`. The error MUST NOT include the full capability grant details, aliases, or URL patterns from `requires:` declarations, as these could reveal infrastructure topology (e.g., internal hostnames, storage bucket names, SSH jump-host addresses). The `ERROR.DATA.required` field MUST be omitted from catch-handler exposure — it MUST only appear in the engine's internal audit log. *(CWE-200)*
>
> **High-security mode:** In high-security mode (engine configuration), `ERROR.DATA.missing` MUST show only the capability category (e.g., `SECRET`, `EXEC`) without the specific entry name. The engine MUST enforce a rate limit of 10 `MissingCapabilityError` instances per flow instance per minute to prevent rapid capability enumeration.
>
> **Normal mode rate limit.** In normal mode (not high-security), the engine MUST enforce a per-flow-instance rate limit of 50 `MissingCapabilityError` events per minute. This prevents capability boundary enumeration through rapid trial-and-error. Exceeding the limit MUST terminate the flow instance with `SecurityViolationError`.

---

#### NotFoundError

| Property | Value |
|---|---|
| **Description** | Referenced resource, record, or endpoint does not exist (HTTP 404 equivalent) |
| **Retryable** | No |
| **Catchable** | Yes |
| **Producing steps** | `request:` action (HTTP 404), `call:` action, `storage:` action (file not found on `get`/`info`/`delete`) |
| **Spec reference** | SS4.1 |

**ERROR.DATA:** None.

---

### 3.3 Configuration Errors (Non-retryable)

---

#### ConfigurationError

| Property | Value |
|---|---|
| **Description** | Invalid service definition, conflicting aliases, missing required engine config |
| **Retryable** | No |
| **Catchable** | Yes |
| **Producing steps** | Flow load-time, service provider initialization |
| **Spec reference** | SS4.1, SS5.2 |

**ERROR.DATA:** None.

---

### 3.4 Execution Errors (Non-retryable)

---

#### AssertionError

| Property | Value |
|---|---|
| **Description** | `assert:` condition evaluated to `false` |
| **Retryable** | No |
| **Catchable** | Yes |
| **Producing steps** | `assert:` directive |
| **Spec reference** | SS3.12 |

**ERROR.DATA:**

| Field | Type | Description |
|---|---|---|
| `condition` | STRING | The assertion expression that failed |

> **Security:** `ERROR.DATA.condition` MUST be truncated to 256 characters. References to `GLOBAL.*`, `CONTEXT.*`, and `SECRET.*` scopes MUST be stripped. Secret redaction MUST be applied.

---

#### CancellationError

| Property | Value |
|---|---|
| **Description** | Running flow instance cancelled by trigger `REPLACE` concurrency mode |
| **Retryable** | No |
| **Catchable** | Yes |
| **Producing steps** | Trigger concurrency (`REPLACE` mode) |
| **Spec reference** | SS2.8 |

**ERROR.DATA:**

| Field | Type | Description |
|---|---|---|
| `reason` | STRING | Cancellation reason (e.g., `"TRIGGER_REPLACE"`) |
| `replacedBy` | STRING | Instance ID of the new instance that replaced this one (if available) |

> **Catch handler behavior:** When a flow instance receives `CancellationError`, its `catch:` handlers execute normally, allowing cleanup logic (releasing locks, closing connections, logging). The `finally:` block also executes. The cancellation is delivered to the currently executing step as an asynchronous interruption.

---

#### StackOverflowError

| Property | Value |
|---|---|
| **Description** | Recursive `run: { flow: CURRENT }` exceeded max depth (default: 100) |
| **Retryable** | No |
| **Catchable** | Yes |
| **Producing steps** | `run:` with `flow: CURRENT` |
| **Spec reference** | SS4.6 |

**ERROR.DATA:**

| Field | Type | Description |
|---|---|---|
| `max_depth` | NUMBER | Configured recursion limit |
| `current_depth` | NUMBER | Depth reached |

---

#### DuplicateInvocationError

| Property | Value |
|---|---|
| **Description** | Flow invocation with duplicate `idempotencyKey:` detected |
| **Retryable** | No |
| **Catchable** | Yes |
| **Producing steps** | Flow invocation (engine-level, before `do:`) |
| **Spec reference** | SS4.4 |

**ERROR.DATA:**

| Field | Type | Description |
|---|---|---|
| `key` | STRING | Idempotency key value |
| `original_invocation_id` | STRING | UUID of first invocation |
| `status` | STRING | `"IN_PROGRESS"` or `"COMPLETED"` |

---

#### CircuitOpenError

| Property | Value |
|---|---|
| **Description** | Circuit breaker in OPEN state; all executions rejected without attempting action |
| **Retryable** | No |
| **Catchable** | Yes |
| **Producing steps** | Any action step with `circuitBreaker:` policy |
| **Spec reference** | SS4.3 |

**ERROR.DATA:**

| Field | Type | Description |
|---|---|---|
| `name` | STRING | Circuit breaker name |
| `scope` | STRING | LOCAL, CONTEXT, or GLOBAL |
| `failures` | NUMBER | Failure count that triggered OPEN |
| `opened_at` | STRING | ISO 8601 timestamp |
| `reset_at` | STRING | ISO 8601 timestamp for HALF_OPEN |

---

#### ConflictError

| Property | Value |
|---|---|
| **Description** | Optimistic locking conflict at transaction commit; concurrent modification detected |
| **Retryable** | No |
| **Catchable** | Yes |
| **Producing steps** | `group: { transaction: true, locking: OPTIMISTIC }` at commit |
| **Spec reference** | SS2.10, SS4.3 |

**ERROR.DATA:**

| Field | Type | Description |
|---|---|---|
| `variables` | ARRAY | Variable names with conflicting writes |
| `source` | STRING | Scope: LOCAL, CONTEXT, or GLOBAL |

**Note:** Always excluded from circuit breaker failure counting. Propagates as `RolledBackError` or `RollbackFailedError` after rollback handlers run.

---

#### DeadlockError

| Property | Value |
|---|---|
| **Description** | Cyclic lock dependency detected; engine aborts one participant |
| **Retryable** | No |
| **Catchable** | Yes |
| **Producing steps** | Concurrent `group:` branches with `lock:` steps creating a cycle |
| **Spec reference** | SS2.10, SS4.3 |

**ERROR.DATA:**

| Field | Type | Description |
|---|---|---|
| `locks` | ARRAY | Lock names involved in the cycle |

**Note:** Always excluded from circuit breaker failure counting.

---

#### GroupError

| Property | Value |
|---|---|
| **Description** | `group:` with `failPolicy: COMPLETE` finished with one or more branch failures |
| **Retryable** | No |
| **Catchable** | Yes |
| **Producing steps** | `group: { mode: PARALLEL\|RACE, failPolicy: COMPLETE }` |
| **Spec reference** | SS4.1 |

**ERROR.DATA:**

| Field | Type | Description |
|---|---|---|
| `failures` | ARRAY | List of `{branch, type, message}` per failed branch |
| `succeeded` | ARRAY | Branch names that completed successfully |

---

### 3.5 Rollback Errors (Non-retryable)

---

#### RolledBackError

| Property | Value |
|---|---|
| **Description** | All rollback handlers completed successfully; original error wrapped as `ERROR.CAUSE` |
| **Retryable** | No |
| **Catchable** | Yes |
| **Producing steps** | Implicit wrapping after successful `rollback:` execution |
| **Spec reference** | SS2.10, SS4.1 |

**ERROR.DATA:**

| Field | Type | Description |
|---|---|---|
| `succeeded` | ARRAY | `_id_`s of handlers that completed |

**Note:** `ERROR.CAUSE` contains the original error that triggered rollback.

---

#### RollbackFailedError

| Property | Value |
|---|---|
| **Description** | One or more rollback handlers threw; some side effects may not be compensated |
| **Retryable** | No |
| **Catchable** | Yes |
| **Producing steps** | Implicit wrapping when `rollback:` handlers partially fail |
| **Spec reference** | SS2.10, SS4.1 |

**ERROR.DATA:**

| Field | Type | Description |
|---|---|---|
| `succeeded` | ARRAY | `_id_`s of handlers that completed |
| `failed` | ARRAY | `_id_`s of handlers that threw |

**Note:** Requires manual intervention or alerting. Distinguish from `RolledBackError` per R-ERR-5.

---

### 3.6 Action-Specific Errors

---

#### HttpError

| Property | Value |
|---|---|
| **Description** | HTTP response with 4xx/5xx status code (when status not mapped to `result:`) |
| **Retryable** | Depends on status (5xx yes, 4xx no) |
| **Catchable** | Yes |
| **Producing steps** | `request:` action |
| **Spec reference** | SS4.6 |

**ERROR.DATA:**

| Field | Type | Description |
|---|---|---|
| `status` | NUMBER | HTTP status code |
| `headers` | MAP | Response headers |
| `body` | ANY | Response body (if available) |

> **Security:** The engine MUST redact the following from `HttpError.DATA` before exposing to catch handlers:
> - `DATA.headers`: MUST strip `Authorization`, `Set-Cookie`, `X-Api-Key`, `Proxy-Authorization`, and any header matching the pattern `*-Token`, `*-Secret`, `*-Key`, `*-Credential`. Remaining headers are permitted. Response headers `Set-Cookie`, `Authorization`, and `WWW-Authenticate` MUST be filtered from `ERROR.DATA.headers` unless the flow explicitly opts in via `request: exposeHeaders: [Set-Cookie, ...]`. All header values MUST be redacted of any resolved secret values (applying the same redaction as for log output).
> - `DATA.body`: MUST apply standard secret redaction — every occurrence of every resolved secret value in the current execution scope MUST be replaced with `[REDACTED]`. HTTP error response bodies are exposed to catch handlers truncated to 4,096 characters (configurable, maximum: 8,192). For responses with `Content-Type: text/html`, the body MUST be omitted entirely (set to `null`) as HTML responses frequently contain session tokens, CSRF tokens, and user data embedded in forms and scripts. Response bodies from requests where `SecretValue`-resolved headers were used MUST be fully redacted (replaced with `[REDACTED]`). Response bodies containing detected credential patterns (JWT tokens, bearer tokens, API keys, connection strings) MUST have the credential portions redacted before exposure to catch handlers.
> *(CWE-200: Exposure of Sensitive Information to an Unauthorized Actor)*
>
> **Redaction/truncation ordering:** Redaction MUST be applied before truncation — the engine MUST first redact all secret values from the body, then truncate to 4,096 characters. When the originating request included any `SecretValue`-resolved headers (`auth.bearer`, `auth.basic`), the engine MUST strip `ERROR.DATA.body` entirely unless the flow explicitly opts in via `captureErrorBody: true` on the request step.

---

#### ExecError

| Property | Value |
|---|---|
| **Description** | Process exited with non-zero code (when `exitCode:` not mapped to `result:`) |
| **Retryable** | No |
| **Catchable** | Yes |
| **Producing steps** | `exec:` action |
| **Spec reference** | SS4.6 |

**ERROR.DATA:**

| Field | Type | Description |
|---|---|---|
| `exitCode` | NUMBER | Process exit code |
| `stderr` | STRING | Process stderr output |
| `stdout` | STRING | Process stdout output |

> **Security:** The engine MUST apply secret redaction (per ENGINE.md item 51) to `ERROR.DATA.stderr` and `ERROR.DATA.stdout` before exposing them to catch handlers. Process stderr frequently contains environment variables, file paths, credentials passed via command-line arguments, and connection strings. Engines MUST truncate stderr and stdout to a maximum of 4,096 characters in ERROR.DATA to prevent information accumulation. *(CWE-209: Generation of Error Message Containing Sensitive Information)*
>
> **Secret value redaction in stderr:** `ERROR.DATA.stderr` MUST be redacted of any resolved secret values before being exposed to `catch:` handlers. Raw stderr MAY contain secrets injected via `env:` (e.g., database passwords, API keys passed as environment variables to the child process). Engines MUST apply the same redaction pass used for log output (per ENGINE.md item 51), replacing every occurrence of every resolved secret value in the current execution scope with `[REDACTED]`. This applies to both stderr and stdout fields in ERROR.DATA. *(CWE-532)*
>
> **Environment variable echo stripping:** When any `SecretValue` was injected via `exec.env`, the engine MUST strip all environment variable echo patterns (`VARNAME=...` where VARNAME matches any injected env key) from stderr and stdout, in addition to exact secret value matching.
>
> **Infrastructure detail stripping:** Before exposing `stderr` to catch handlers, the engine MUST apply the following sanitization in addition to secret redaction: (1) strip IPv4 addresses matching private ranges (`10.*`, `172.16-31.*`, `192.168.*`, `127.*`), (2) strip hostnames matching engine-configured internal domain patterns (e.g., `*.internal`, `*.local`, `*.corp`), (3) strip file paths matching engine-configured sensitive path prefixes (e.g., `/etc/`, `/home/`, `C:\Users\`). Stripping replaces matched values with `[FILTERED]`. The engine MUST provide configuration for additional strip patterns. *(CWE-209)*

---

#### MailError

| Property | Value |
|---|---|
| **Description** | Mail sending failed (SMTP, routing, or policy error) |
| **Retryable** | No |
| **Catchable** | Yes |
| **Producing steps** | `mail:` action |
| **Spec reference** | SS4.6 |

**ERROR.DATA:** None.

---

#### SmtpError

| Property | Value |
|---|---|
| **Description** | SMTP protocol-level error (child of `MailError`) |
| **Retryable** | No |
| **Catchable** | Yes (also caught by `MailError` catch) |
| **Producing steps** | `mail:` action (SMTP phase) |
| **Spec reference** | SS4.6 |

**ERROR.DATA:** None.

---

#### AddressError

| Property | Value |
|---|---|
| **Description** | Invalid email address format or domain lookup failure (child of `MailError`) |
| **Retryable** | No |
| **Catchable** | Yes (also caught by `MailError` catch) |
| **Producing steps** | `mail:` action (address validation) |
| **Spec reference** | SS4.6 |

**ERROR.DATA:** None.

---

#### StorageError

| Property | Value |
|---|---|
| **Description** | Storage operation failed (remote I/O error, permission, quota, protocol error) |
| **Retryable** | No (transient network issues use `ConnectionError`/`TimeoutError`) |
| **Catchable** | Yes |
| **Producing steps** | `storage:` action |
| **Spec reference** | SS4.6 |

**ERROR.DATA:**

| Field | Type | Description |
|---|---|---|
| `url` | STRING | Resolved storage URL (or alias if unresolved) |
| `operation` | STRING | Operation that failed |
| `path` | STRING | Remote path involved |
| `reason` | STRING | Provider-specific failure reason (e.g., `"already_exists"`, `"quota_exceeded"`, `"protocol_error"`) |

For `transfer` operations, additional fields:

| Field | Type | Description |
|---|---|---|
| `source_url` | STRING | Source storage URL (or alias) |
| `target_url` | STRING | Target storage URL (or alias) |
| `source_path` | STRING | Source path |
| `target_path` | STRING | Target path |
| `failed_side` | STRING | `"source"` or `"target"` |

> **Security:** The engine MUST sanitize `StorageError.DATA.url` by removing the `userinfo` component (username:password) before exposure — any `username:password@` prefix in the authority component MUST be stripped entirely. Query parameters containing tokens or signatures (matching patterns `*token*`, `*sig*`, `*signature*`, `*key*`, `*secret*`, `*credential*`, `*auth*`, `*sas*`) MUST be redacted by replacing their values with `[REDACTED]` while preserving the parameter name for debugging (e.g., `?sig=[REDACTED]&prefix=foo`). All other query parameters and fragments MUST also be removed. The URL scheme, host, and path are permitted for debugging. The same sanitization MUST be applied to `source_url` and `target_url` fields for transfer operations. For `file://` URLs, the path MUST be relativized to the storage root (stripping the absolute filesystem prefix) to prevent infrastructure topology disclosure. *(CWE-200)*
>
> **Infrastructure topology redaction.** For S3 URLs, the engine MUST redact the bucket name from `url` and `source_url`/`target_url` fields, replacing it with `[BUCKET]`. For SFTP/FTP URLs, the engine MUST redact the hostname, replacing it with `[HOST]`. This prevents infrastructure topology enumeration via error responses. The scheme and path structure MAY be retained for debugging purposes.

---

#### StoragePathError

| Property | Value |
|---|---|
| **Description** | Invalid storage path: traversal attempt (`../`), null bytes, or path format violation (child of `StorageError`) |
| **Retryable** | No |
| **Catchable** | Yes (also caught by `StorageError` catch) |
| **Producing steps** | `storage:` action (path validation) |
| **Spec reference** | SS4.6 |

**ERROR.DATA:** Inherits `StorageError` fields.

---

#### SshError

| Property | Value |
|---|---|
| **Description** | Remote command exited with non-zero code (when `exitCode` not mapped in `result:`) |
| **Retryable** | No |
| **Catchable** | Yes |
| **Producing steps** | `ssh:` action |
| **Spec reference** | SS4.6 |

**ERROR.DATA:**

| Field | Type | Description |
|---|---|---|
| `exitCode` | NUMBER | Remote process exit code |
| `stderr` | STRING | Remote process stderr output |
| `stdout` | STRING | Remote process stdout output |

> **Security:** The engine MUST apply secret redaction (per ENGINE.md item 51) to `ERROR.DATA.stderr` and `ERROR.DATA.stdout` before exposing them to catch handlers. Process stderr frequently contains environment variables, file paths, credentials passed via command-line arguments, and connection strings. Engines MUST truncate stderr and stdout to a maximum of 4,096 characters in ERROR.DATA to prevent information accumulation. *(CWE-209: Generation of Error Message Containing Sensitive Information)*
>
> **Secret value redaction in stderr:** `ERROR.DATA.stderr` MUST be redacted of any resolved secret values before being exposed to `catch:` handlers. Raw stderr MAY contain secrets injected via `env:` (e.g., database passwords, API keys passed as environment variables to the remote command). Engines MUST apply the same redaction pass used for log output (per ENGINE.md item 51), replacing every occurrence of every resolved secret value in the current execution scope with `[REDACTED]`. This applies to both stderr and stdout fields in ERROR.DATA. *(CWE-532)*
>
> **Environment variable echo stripping:** When any `SecretValue` was injected via `ssh.env`, the engine MUST strip all environment variable echo patterns (`VARNAME=...` where VARNAME matches any injected env key) from stderr and stdout, in addition to exact secret value matching.
>
> **Infrastructure detail stripping:** Before exposing `stderr` to catch handlers, the engine MUST apply the following sanitization in addition to secret redaction: (1) strip IPv4 addresses matching private ranges (`10.*`, `172.16-31.*`, `192.168.*`, `127.*`), (2) strip hostnames matching engine-configured internal domain patterns (e.g., `*.internal`, `*.local`, `*.corp`), (3) strip file paths matching engine-configured sensitive path prefixes (e.g., `/etc/`, `/home/`, `C:\Users\`). Stripping replaces matched values with `[FILTERED]`. The engine MUST provide configuration for additional strip patterns. *(CWE-209)*

**Auto-throw suppression:** When `exitCode` is mapped in `result:`, `SshError` is not thrown (same pattern as `ExecError`).

---

### 3.7 Data / Encoding Errors (Non-retryable)

---

#### ParseError

| Property | Value |
|---|---|
| **Description** | Malformed content for declared format (JSON, YAML, XML, CSV, TSV) |
| **Retryable** | No |
| **Catchable** | Yes |
| **Producing steps** | `request: parseAs:` auto-decode, `exec: parseAs:` auto-decode, CEL `decode(FORMAT)`, `parse(FORMAT)` |
| **Spec reference** | SS4.6 |

**ERROR.DATA:** None.

---

#### EncodeError

| Property | Value |
|---|---|
| **Description** | Value out of range for target encoding format |
| **Retryable** | No |
| **Catchable** | Yes |
| **Producing steps** | CEL `encode(FORMAT)` |
| **Spec reference** | SS4.6 |

**ERROR.DATA:** None.

---

#### XPathError

| Property | Value |
|---|---|
| **Description** | XPath 1.0 expression syntax error or evaluation failure |
| **Retryable** | No |
| **Catchable** | Yes |
| **Producing steps** | CEL `string.xpath(expr)`, `string.xpathAll(expr)` |
| **Spec reference** | SS4.6 |

**ERROR.DATA:** None.

---

### 3.8 Resource Limit Errors (Non-retryable)

---

#### ResourceExhaustedError

| Property | Value |
|---|---|
| **Description** | System resource limit exceeded (response size, XML doc size, lock registry) |
| **Retryable** | No |
| **Catchable** | Yes |
| **Producing steps** | `request:` (response > limit), XML parsing (doc > limit), `lock:` (registry > 10k) |
| **Spec reference** | SS4.3, SS4.6 |

**ERROR.DATA:** None.

---

#### ResourceLimitError

| Property | Value |
|---|---|
| **Description** | User-configured resource bound exceeded (maxIterations, maxItems, maxDuration, parallel branch limit, CEL string size limit) |
| **Retryable** | No |
| **Catchable** | Yes |
| **Producing steps** | `forEach` (maxItems), `while`/`repeat` (maxIterations), `wait` (maxDuration), `parallel` (branch limit), CEL evaluation (string output size) |
| **Spec reference** | SS3.3, SS3.4, SS3.5, SS3.15 |

**ERROR.DATA:**

| Field | Type | Description |
|---|---|---|
| `limit` | INTEGER | The configured limit that was exceeded |
| `actual` | INTEGER | The actual value that exceeded the limit |
| `parameter` | STRING | The parameter name that defines the limit (e.g., `maxIterations`, `maxItems`, `maxDuration`) |

---

#### ResourceNotFoundError

| Property | Value |
|---|---|
| **Description** | Declared resource not available at runtime |
| **Retryable** | No |
| **Catchable** | Yes |
| **Producing steps** | `RESOURCES.*` access |
| **Spec reference** | SS4.6 |

**ERROR.DATA:** None.

---

### 3.9 Secret Errors (Non-retryable)

---

#### SecretAccessError

| Property | Value |
|---|---|
| **Description** | Raised when a secret cannot be resolved — either because it does not exist in the provider or because the provider's ACL denies access. The engine MUST NOT expose which reason applies at the catch-handler level. The engine's internal audit log MUST still distinguish between not-found and access-denied for operator debugging. *(CWE-204: Observable Response Discrepancy)* |
| **Retryable** | No |
| **Catchable** | Yes |
| **Producing steps** | `SECRET.*` access at action boundary |
| **Spec reference** | SS5.3 |

**ERROR.DATA:** None.

---

#### SecretTypeError

| Property | Value |
|---|---|
| **Description** | Invalid field access on secret (e.g., `.username` on a `text`-type secret) |
| **Retryable** | No |
| **Catchable** | Yes |
| **Producing steps** | `SECRET.*` field access |
| **Spec reference** | SS5.3 |

**ERROR.DATA:** None.

---

#### SecretRateLimitError

| Property | Value |
|---|---|
| **Description** | Per-tenant SECRET resolution rate limit exceeded |
| **Retryable** | No |
| **Catchable** | Yes |
| **Producing steps** | `SECRET.*` access when rate limit is exceeded |
| **Spec reference** | SS5.3 |

**ERROR.DATA:**

| Field | Type | Description |
|---|---|---|
| `limit` | INTEGER | Configured rate limit |
| `window` | STRING | Rate limit window (e.g., `"1s"`) |
| `tenant` | STRING | Tenant identifier |

---

#### SecretInjectionError

| Property | Value |
|---|---|
| **Description** | Raised when a resolved `SecretValue` is detected in a prohibited position (e.g., URL query parameter) during request construction. *(CWE-598)* |
| **Retryable** | No |
| **Catchable** | Yes |
| **Producing steps** | `request:` construction when secret appears in query parameter position |
| **Spec reference** | SA-SECRET-18, SECRETS.md §7.2 |

**ERROR.DATA:**

| Field | Type | Description |
|---|---|---|
| `position` | STRING | The prohibited position where the secret was detected (e.g., `"query"`) |
| `param_name` | STRING | The parameter name containing the secret value |

---

#### SecretConfigurationError

| Property | Value |
|---|---|
| **Description** | Raised during flow registration or engine startup when a declared secret is not found in the provider, or when secret file permissions violate security requirements (e.g., world-readable secret files). *(CWE-312, CWE-732)* |
| **Retryable** | No |
| **Catchable** | No (deployment-time / startup error; fires during flow registration before the flow body executes) |
| **Producing steps** | Flow registration (secret existence validation), engine startup (file permission check) |
| **Spec reference** | SECRETS.md §4.2 (FileSecretsProvider file permissions), §9 (strict vs optional secrets) |

**ERROR.DATA:**

| Field | Type | Description |
|---|---|---|
| `secret_name` | STRING | The name of the secret that failed validation |
| `reason` | STRING | The failure reason: `"not_found"`, `"insecure_permissions"`, or `"symlink_target_insecure"` |

---

### 3.10 Non-Catchable Errors (Engine-Level)

These errors occur before or outside the flow body. They cannot be handled by `catch:` clauses.

**Note on `async: true` sub-flows:** Catchable errors (including security errors like `MissingCapabilityError`) thrown inside `async: true` sub-flows are NOT propagated to the parent flow's `catch:` handlers. They are logged (security errors unconditionally per [FLOWMARKUP-ENGINE.md](FLOWMARKUP-ENGINE.md) §5.4 item 18), but the parent flow continues unaware. This is distinct from non-catchable errors — the errors themselves remain catchable within the async sub-flow's own `catch:` handlers, but fire-and-forget semantics prevent cross-flow propagation.

While async security errors are not propagated to `catch:` handlers, they are counted in `RUNTIME.ASYNC_SECURITY_ERRORS` and unconditionally logged per [FLOWMARKUP-ENGINE.md](FLOWMARKUP-ENGINE.md) §5.4 item 20. Flow authors can poll this counter to detect async security violations programmatically.

### Fail-Closed Error Handling

When the engine's own error-handling machinery encounters an unexpected failure (e.g., OOM during redaction, regex timeout during secret matching, the redaction function itself throws, the audit logger is unavailable, the checkpoint system fails during error persistence), the engine MUST fail-closed: suppress the original error data entirely and substitute a generic error with no data payload, rather than risk exposing unredacted content. Specifically, the engine MUST:

1. **Suppress original error data entirely** — the original error (which may contain unredacted secrets, stack traces, or internal state) MUST NOT be exposed to `catch:` handlers, `finally:` blocks, or any CEL expression
2. **Substitute a generic error** — return a synthetic error to the flow and any external caller: `{TYPE: "EngineError", MESSAGE: "Internal engine error", DATA: null, CAUSE: null}` with no data payload
3. **Halt the affected flow instance** immediately (transition to FAILED state)
4. **Write the full, unredacted error** to the engine's local emergency log (a secondary log path that does not depend on the primary audit logging infrastructure). If the emergency log itself is unavailable, the engine MUST still suppress the error data — logging failure does not override the fail-closed requirement
5. **NOT expose any internal error details**, stack traces, partial state, or partially-redacted content to the flow's catch handlers. Partial redaction (where some but not all secrets were replaced before the failure) is treated as no redaction

This fail-closed behavior prevents information leakage when the engine's security mechanisms are themselves compromised. The principle is: *it is always safer to lose error diagnostics than to leak secrets*. *(CWE-636: Not Failing Securely)*

---

#### FlowVersionError

| Property | Value |
|---|---|
| **Description** | Flow content changed at checkpoint resume, no `onVersionChange:` declared |
| **Retryable** | N/A |
| **Catchable** | No |
| **Producing steps** | Checkpoint resume, `run:` with `integrity:` |
| **Spec reference** | SS2.11, SS5.1 |

**ERROR.DATA:**

| Field | Type | Description |
|---|---|---|
| `location` | STRING | Canonical flow URL/path |
| `expected_hash` | STRING | Hash from `integrity:` or checkpoint |
| `actual_hash` | STRING | Computed hash (null if unreachable) |

---

#### FlowFetchError

| Property | Value |
|---|---|
| **Description** | Remote flow fetch failed — authentication exhausted, network unreachable, or invalid URL |
| **Retryable** | N/A |
| **Catchable** | No |
| **Producing steps** | `run:` with remote `flow:` URL (github://, https://) |
| **Spec reference** | SS5.1 |

**ERROR.DATA:**

| Field | Type | Description |
|---|---|---|
| `location` | STRING | Flow URL that failed to fetch |
| `reason` | STRING | Failure reason (e.g., `"authentication_failed"`) |

---

#### MigrationError

| Property | Value |
|---|---|
| **Description** | `onVersionChange:` handler threw or encountered unrecoverable issue; checkpoint preserved |
| **Retryable** | N/A |
| **Catchable** | No |
| **Producing steps** | `onVersionChange:` handler execution |
| **Spec reference** | SS2.11 |

**ERROR.DATA:**

| Field | Type | Description |
|---|---|---|
| `location` | STRING | Canonical flow URL/path |
| `old_version` | NUMBER | Version from checkpoint |
| `new_version` | NUMBER | Version from loaded flow |
| `step_cursor` | STRING | Execution position (null if handler threw) |
| `cause` | STRING | Wrapped error message |

**Note:** `return` inside `onVersionChange:` is NOT a MigrationError -- it gracefully terminates the flow (checkpoint discarded, `finally:` runs).

---

#### VersionParseError

| Property | Value |
|---|---|
| **Description** | Version string cannot be parsed by the `version()` CEL function |
| **Retryable** | N/A |
| **Catchable** | No |
| **Producing steps** | CEL `version()` function |
| **Spec reference** | N/A |

**ERROR.DATA:** None.

> **Design rationale:** `VersionParseError` is non-catchable because the `version()` function is intended for structured version comparisons where invalid input indicates a programming error (wrong field, wrong format), not a recoverable runtime condition. To safely handle user-provided version strings, validate the format with a regex (`s.matches('^\\d+\\.\\d+\\.\\d+.*$')`) before calling `version()`.

---

#### SchemaLoadError

| Property | Value |
|---|---|
| **Description** | External schema file not found or malformed during type resolution |
| **Retryable** | N/A |
| **Catchable** | No |
| **Producing steps** | `types:` section with external `$ref` |
| **Spec reference** | N/A |

**ERROR.DATA:** None.

---

#### SchemaResolutionError

| Property | Value |
|---|---|
| **Description** | Schema `$ref` cannot be resolved (circular reference, unknown type) |
| **Retryable** | N/A |
| **Catchable** | No |
| **Producing steps** | `types:` schema resolution |
| **Spec reference** | N/A |

**ERROR.DATA:** None.

---

#### UnsupportedProviderError

| Property | Value |
|---|---|
| **Description** | Flow references a service provider the engine does not implement (e.g., storage backend type not available, SSH not supported) |
| **Retryable** | N/A |
| **Catchable** | No |
| **Producing steps** | Flow load time — service initialization |
| **Spec reference** | SS5.4 |

**ERROR.DATA:**

| Field | Type | Description |
|---|---|---|
| `provider` | STRING | Provider identifier (e.g., storage backend type) |
| `service` | STRING | Service alias or URL that references the unsupported provider |

---

#### EngineError

| Property | Value |
|---|---|
| **Description** | Generic error substituted when the engine's error-handling machinery fails (fail-closed behavior) |
| **Retryable** | No |
| **Catchable** | No |
| **Producing steps** | Engine internal failure during error handling (OOM during redaction, audit logger unavailable, checkpoint system failure) |
| **Spec reference** | ERRORS.md Fail-Closed Error Handling |

**ERROR.DATA:** None (explicitly `null` by design -- no data payload to prevent information leakage).

> `EngineError` is a last-resort substitute. The original error (which may contain unredacted secrets) is suppressed entirely. The flow instance is halted and transitioned to FAILED state.

---

#### SecurityViolationError

| Property | Value |
|---|---|
| **Description** | Flow instance terminated due to security policy violation |
| **Retryable** | No |
| **Catchable** | No |
| **Producing steps** | MissingCapabilityError rate limit exceeded (50/min normal mode, 10/min high-security mode), `securityParity: false` outside valid test context |
| **Spec reference** | ERRORS.md MissingCapabilityError rate limits, TESTING.md |

**ERROR.DATA:**

| Field | Type | Description |
|---|---|---|
| `reason` | STRING | Violation type (e.g., `"CAPABILITY_ENUMERATION_RATE_LIMIT"`, `"SECURITY_PARITY_VIOLATION"`) |

> `SecurityViolationError` terminates the flow instance immediately. It is not catchable because the security boundary has been breached and continued execution is unsafe.

---

#### TruncatedCauseError

| Property | Value |
|---|---|
| **Description** | Synthetic marker replacing truncated entries in an ERROR.CAUSE chain |
| **Retryable** | No |
| **Catchable** | No (never thrown as a top-level error; only appears as a nested CAUSE entry) |
| **Producing steps** | Engine CAUSE chain truncation when depth exceeds limit (default: 16, max: 64) |
| **Spec reference** | ERRORS.md ERROR.CAUSE chain limits, ENGINE.md item 62 |

**ERROR.DATA:** None (explicitly `null`).

> `TruncatedCauseError` is never raised as a standalone error. It only appears as the deepest entry in an ERROR.CAUSE chain when the chain exceeds the configured depth limit. The MESSAGE field indicates how many additional causes were omitted. Original causes are preserved in the engine's internal audit log.

---

## 4. Testing Framework Errors

These errors are only thrown in test execution mode (see `FLOWMARKUP-TESTING.md`).

---

#### UnmockedServiceError

| Property | Value |
|---|---|
| **Description** | Test `mode: UNIT`: `call:` invoked service without matching mock |
| **Retryable** | No |
| **Catchable** | Yes |
| **ERROR.DATA** | `service` (string): service alias invoked; `operation` (string): operation name; `step_id` (string): step `_id_` |

---

#### UnmockedResourceError

| Property | Value |
|---|---|
| **Description** | Test `mode: UNIT`: resource accessed but not injected |
| **Retryable** | No |
| **Catchable** | Yes |
| **ERROR.DATA** | `resource` (string): resource name; `step_id` (string): step `_id_` |

---

#### UnmockedStorageError

| Property | Value |
|---|---|
| **Description** | Test `mode: UNIT`: `storage:` invoked without matching mock |
| **Retryable** | No |
| **Catchable** | Yes |
| **ERROR.DATA** | `url` (string): storage URL or alias; `operation` (string): storage operation (get, put, delete, etc.); `step_id` (string): step `_id_` |

---

#### UnmockedSshError

| Property | Value |
|---|---|
| **Description** | Test `mode: UNIT`: `ssh:` invoked without matching mock |
| **Retryable** | No |
| **Catchable** | Yes |
| **ERROR.DATA** | `host` (string): SSH host or alias; `command` (string): command invoked; `step_id` (string): step `_id_` |

---

#### RecordingMismatchError

| Property | Value |
|---|---|
| **Description** | Test `replay:`: action call does not match recording sequence |
| **Retryable** | No |
| **Catchable** | Yes |
| **ERROR.DATA** | `expected_index` (integer): expected position in recording sequence; `expected_action` (string): expected action type and target; `actual_action` (string): actual action type and target; `step_id` (string): step `_id_` |

---

#### CapabilityMismatchError

| Property | Value |
|---|---|
| **Description** | Test mock `assertCapabilities:` failed; actual capabilities differ from expected |
| **Retryable** | No |
| **Catchable** | Yes |
| **ERROR.DATA** | `expected` (map): expected capability declarations; `actual` (map): actual effective capabilities; `step_id` (string): step `_id_`; `mock_id` (string): mock identifier |

---

#### TemplateInjectionError

| Property | Value |
|---|---|
| **Description** | Test template substitution caused structural divergence between the template and the substituted result. Post-parse substitution is required to prevent YAML injection. See FLOWMARKUP-TESTING.md. |
| **Retryable** | No |
| **Catchable** | Yes |
| **ERROR.DATA** | `expression` (string): template expression that caused divergence; `field` (string): field path where substitution occurred |

---

#### TemplateAccessViolationError

| Property | Value |
|---|---|
| **Description** | Test template expression attempted to access an out-of-scope variable. Engines MUST restrict template expression scope. See FLOWMARKUP-TESTING.md. |
| **Retryable** | No |
| **Catchable** | Yes |
| **ERROR.DATA** | `expression` (string): template expression; `variable` (string): out-of-scope variable accessed; `allowed_scope` (array of string): variables permitted in scope |

---

#### PathTraversalError

| Property | Value |
|---|---|
| **Description** | Test fixture or resource path attempted directory traversal. Path traversal attempts MUST cause an immediate test failure. See FLOWMARKUP-TESTING.md. |
| **Retryable** | No |
| **Catchable** | No (immediate test failure) |
| **ERROR.DATA** | `path` (string): requested path (redacted if sensitive); `base_directory` (string): base directory boundary that was violated |

---

## 5. Quick Reference

### Retryable Errors

| Error | Producing Steps |
|---|---|
| `TimeoutError` | Any action with `timeout:`, `lock:`, `waitFor:`, `waitUntil:` |
| `ConnectionError` | `request:`, `storage:`, `ssh:` |
| `TLSError` | `request:` (HTTPS), `storage:` (FTPS, HTTPS S3), `ssh:` |
| `RateLimitError` | `rateLimit:` policy, `request:` (HTTP 429) |
| `EventBufferFullError` | `emit:` |
| `ServiceUnavailableError` | `call:`, inline `SERVICES.*` |
| `ServiceError` | `call:`, inline `SERVICES.*` |

### Non-Retryable Errors (Catchable)

| Error | Producing Steps |
|---|---|
| `ValidationError` | Input contract, `set:`, `params:`, auto-decode |
| `BadRequestError` | `request:` (HTTP 4xx), `call:` |
| `AuthenticationError` | `request:` (401), `call:`, `mail:`, `storage:`, `ssh:` |
| `AccessDeniedError` | `request:` (403), `call:`, `storage:`, `ssh:` |
| `MissingCapabilityError` | `run:`, `request:`, `exec:`, `mail:`, `storage:`, `ssh:`, scope access |
| `NotFoundError` | `request:` (404), `call:`, `storage:` (file not found) |
| `ConfigurationError` | Flow load-time |
| `AssertionError` | `assert:` |
| `CancellationError` | Trigger concurrency (`REPLACE` mode) |
| `StackOverflowError` | `run: { flow: CURRENT }` |
| `DuplicateInvocationError` | `idempotencyKey:` |
| `CircuitOpenError` | `circuitBreaker:` policy |
| `ConflictError` | `group: { transaction: true, locking: OPTIMISTIC }` |
| `DeadlockError` | Concurrent `lock:` cycle |
| `GroupError` | `group: { failPolicy: COMPLETE }` |
| `RolledBackError` | `rollback:` handlers (all succeeded) |
| `RollbackFailedError` | `rollback:` handlers (some failed) |
| `HttpError` | `request:` (4xx/5xx) |
| `ExecError` | `exec:` (non-zero exit) |
| `MailError` | `mail:` |
| `SmtpError` (child of `MailError`) | `mail:` (SMTP protocol) |
| `AddressError` (child of `MailError`) | `mail:` (address validation) |
| `StorageError` | `storage:` (remote I/O, permission, quota) |
| `StoragePathError` (child of `StorageError`) | `storage:` (path traversal, invalid path) |
| `SshError` | `ssh:` (non-zero exit) |
| `ParseError` | `decode()`/`parse()`, auto-decode |
| `EncodeError` | `encode()` |
| `XPathError` | `xpath()`/`xpathAll()` |
| `ResourceExhaustedError` | Response/document size limits |
| `ResourceLimitError` | `forEach` (maxItems), `while`/`repeat` (maxIterations), `wait` (maxDuration), `parallel` (branch limit), CEL (string size) |
| `ResourceNotFoundError` | `RESOURCES.*` access |
| `SecretAccessError` | `SECRET.*` access |
| `SecretTypeError` | `SECRET.*` field access |
| `SecretRateLimitError` | `SECRET.*` rate limit exceeded |
| `SecretInjectionError` | `SECRET.*` in prohibited position (query param) |

### Non-Catchable Errors (Engine-Level)

| Error | Trigger |
|---|---|
| `EngineError` | Engine error-handling machinery failure (fail-closed) |
| `FlowVersionError` | Content hash mismatch at resume |
| `FlowFetchError` | Remote flow fetch failed (auth, network) |
| `MigrationError` | `onVersionChange:` handler threw |
| `SecurityViolationError` | MissingCapabilityError rate limit, `securityParity` violation |
| `TruncatedCauseError` | ERROR.CAUSE chain depth exceeds limit |
| `VersionParseError` | `version()` CEL function |
| `SchemaLoadError` | External schema not found |
| `SchemaResolutionError` | `$ref` resolution failure |
| `UnsupportedProviderError` | Flow references unsupported provider (storage type, SSH) |
| `SecretConfigurationError` | Secret not found or insecure file permissions at deployment |

---

## 6. User-Defined Error Patterns

### When to Create Custom Hierarchies

- **Domain errors** -- when business logic failures need specific handling (e.g., `PaymentDeclinedError`, `InsufficientInventoryError`)
- **Error data** -- when the `catch:` handler needs structured context beyond the message
- **Polymorphic catch** -- when multiple related errors should be catchable by a common parent

### Naming Conventions

Use a domain prefix followed by `Error`:

```yaml
throws:
- PaymentDeclinedError
- PaymentExpiredError
- $kind: InsufficientFundsError
  $parent: PaymentDeclinedError
  data:
    available_balance: NUMBER
    required_amount: NUMBER
```

### When to Use `$parent:` vs Flat Types

- **Use `$parent:`** when a `catch:` handler should handle a family of related errors (e.g., catch `PaymentError` to handle both `PaymentDeclinedError` and `PaymentExpiredError`)
- **Use flat types** when each error is independent and handlers are specific

### Error Data Schema Design

Keep `data:` fields focused on what the `catch:` handler needs to decide its response:

```yaml
throws:
- $kind: OrderValidationError
  $parent: ValidationError
  data:
    field: STRING          # which field failed
    constraint: STRING     # what constraint was violated
    value: ANY             # the offending value
```
