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:
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
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:| 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. |
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 aSessionError(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 beyondassert-guarded print calls. Hosts that need structured logging should subscribe to the Stream<SessionEvent> and log at the call site.
Related
- Lifecycle — when
SessionErrorfires and the resulting state. - Watch Protocol —
ErrorCodeproto enum.