> ## Documentation Index
> Fetch the complete documentation index at: https://docs.synheart.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Errors

> SessionError event codes, exception classes, and recovery paths

The session SDK carries two error surfaces: in-stream `SessionError` events that close a running stream, and synchronous exception classes thrown for API misuse before/during a `Stream<SessionEvent>` is established. They are aligned across Dart, Kotlin, and Swift.

## `SessionErrorCode` (in-stream)

Wire enum from `session.proto`:

```protobuf theme={null}
enum ErrorCode {
  ERROR_UNSPECIFIED         = 0;
  ERROR_PERMISSION_DENIED   = 1;
  ERROR_SENSOR_UNAVAILABLE  = 2;
  ERROR_LOW_BATTERY         = 3;
  ERROR_OS_TERMINATED       = 4;
  ERROR_INVALID_STATE       = 5;
}
```

SDK projection — wire string is the snake\_case JSON value the platform layer ships:

| Dart `SessionErrorCode` | Wire string          | Cause                                                                                 |
| ----------------------- | -------------------- | ------------------------------------------------------------------------------------- |
| `permissionDenied`      | `permission_denied`  | OS denied a sensor permission (HealthKit, Health Connect, BLE, etc.).                 |
| `sensorUnavailable`     | `sensor_unavailable` | No paired wearable; HR sensor missing or returning no data.                           |
| `lowBattery`            | `low_battery`        | Watch battery dropped below the OS threshold for an active workout/exercise session.  |
| `osTerminated`          | `os_terminated`      | OS killed the foreground/exercise session (memory pressure, user backgrounded, etc.). |
| `invalidState`          | `invalid_state`      | API misuse: `StartSession` while already active, unknown `session_id`, etc.           |

`SessionErrorCode.fromString(value)` (all three SDKs) parses the wire string. Unknown values throw `ArgumentError` in Dart.

## `SessionError` event

```dart theme={null}
class SessionError extends SessionEvent {
  final SessionErrorCode code;
  final String message;
}
```

`SessionError` always closes the stream. After it fires, the same `sessionId` can be started again with a fresh `SessionConfig`.

`message` is human-readable and platform-specific. Hosts should log it but should not parse it for control flow — only `code` is stable.

## Recovery paths

| Code                 | Recovery                                                                                                                                                                   |
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `permission_denied`  | Prompt for the missing permission via the appropriate platform API; restart the session. The SDK does not retry.                                                           |
| `sensor_unavailable` | Reconnect the wearable; verify `getWatchStatus()` reports `paired = true`, `reachable = true`, `installed = true`. Switch to a different `BiosignalProvider` if available. |
| `low_battery`        | User remediation. The SDK does not throttle or downsample.                                                                                                                 |
| `os_terminated`      | Resume the session with a new `sessionId`. The OS-killed session is gone; partial summaries are not recoverable from the SDK side.                                         |
| `invalid_state`      | Programmer error. Inspect `message` and the call site. Common causes: starting two sessions with the same id, calling on a disposed `SynheartSession`.                     |

## Dart synchronous exceptions

For misuse that doesn't reach the engine, the Dart SDK throws:

```dart theme={null}
class SynheartSessionError implements Exception { ... }
class SessionPermissionDeniedError extends SynheartSessionError { ... }
class SessionSensorUnavailableError extends SynheartSessionError { ... }
class SessionInvalidStateError extends SynheartSessionError { ... }
```

| Exception                       | When                                                                                                             |
| ------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| `SessionInvalidStateError`      | `startSession` after `dispose()`. `startSession` with a `sessionId` already running. Constructor preconditions.  |
| `SessionPermissionDeniedError`  | Permission probe at construction time (rare; most permission failures surface as `SessionError` events instead). |
| `SessionSensorUnavailableError` | No usable provider at construction.                                                                              |

These do not reach the event stream — they're thrown directly from `SynheartSession.startSession(...)` and friends.

## Distinguishing the two surfaces

| Situation                                           | Surface                                                                            |
| --------------------------------------------------- | ---------------------------------------------------------------------------------- |
| You called `startSession` with a duplicate id       | Dart `SessionInvalidStateError` thrown synchronously.                              |
| You called `startSession` after `dispose()`         | Dart `SessionInvalidStateError` thrown synchronously.                              |
| OS denied HealthKit at session start                | `SessionError(permissionDenied, ...)` event on the stream.                         |
| Watch went out of range                             | (no error — channel errors are swallowed; the engine continues with empty buffers) |
| Watch ran out of battery mid-session                | `SessionError(lowBattery, ...)` event on the stream.                               |
| User backgrounded the app and OS killed the workout | `SessionError(osTerminated, ...)` event on the stream.                             |

## Channel error swallowing

The Watch event channel listener treats channel transport errors as non-fatal — dropped frames during a brief disconnect are preferable to forcing the host to handle a `SessionError(osTerminated)` for every Bluetooth blip. Apps that need strict reachability semantics must poll `getWatchStatus()` and stop the session manually on persistent disconnect.

## Logging

The SDK does not write to platform log streams beyond `assert`-guarded `print` calls. Hosts that need structured logging should subscribe to the `Stream<SessionEvent>` and log at the call site.

## Related

* [Lifecycle](/synheart-session/lifecycle) — when `SessionError` fires and the resulting state.
* [Watch Protocol](/synheart-session/watch-protocol) — `ErrorCode` proto enum.
