Error handling
The SDK distinguishes two failure regimes: transient (worth retrying) and fatal (stop the client). Your code should handle them differently.
Two paths into errors
Section titled “Two paths into errors”Errors reach you through two channels:
connect()rejects — only on fatal errors. Transient errors keep the promise pending while reconnect attempts run.errorevents — both fatal and transient surface here, distinguished by thefatal: booleanflag.
client.on('error', ({ message, fatal }) => { if (fatal) console.error('stop:', message) else console.warn('transient:', message)})
try { await client.connect()} catch (err) { // only reached on fatal — same message as the fatal `error` event}Fatal errors
Section titled “Fatal errors”The SDK considers an error fatal when:
- The server closed with
4001(missing apiKey),4002(invalid apiKey), or4003(quota exceeded). - The server reported an incompatible protocol version.
maxAttemptsreconnect attempts have been exhausted.
When fatal:
- The reconnect loop stops.
errorfires withfatal: true.- The pending
connect()promise (if any) rejects. - The client transitions to
connectionState.status: 'disconnected'. - Calling
disconnect()is safe but optional — the SDK is already idle.
To recover, construct a new client (createClient(...) again). The old instance is dead.
Transient errors
Section titled “Transient errors”Transient errors include WebSocket drops, network blips, gateway restarts. The SDK reconnects with exponential backoff (see Reconnect tuning) and emits:
disconnected: { willReconnect: true, code, reason }when the connection drops.reconnecting: { attempt, delayMs }before each reconnect attempt.connected(no payload) on successful reconnection.- Sometimes
error: { fatal: false, message }for low-level transport errors that the SDK is already retrying.
Most apps don’t need to react to transient error events — the reconnect machinery is already handling them. Surface them only for debugging or telemetry.
Detecting stale data
Section titled “Detecting stale data”While the SDK reconnects, your in-memory state is frozen — client.snapshot() returns the last known state with stale: true. Display this in your UI:
function isStale() { return client.snapshot().stale}For a “last update was N seconds ago” indicator on individual rows:
const lastSeenAt = new Map<SportEventId, number>()
client.on('sportEvent:updated', ({ sportEvent, receivedAt }) => { lastSeenAt.set(sportEvent.id, receivedAt)})
// somewhere in your UI:const ageMs = Date.now() - (lastSeenAt.get(eventId) ?? 0)receivedAt is the SDK’s local clock — see Time semantics.
Handling errors inside listeners
Section titled “Handling errors inside listeners”Throwing from a listener does not crash the SDK. The emitter catches and logs (to console.error). But it’s still your problem — wrap risky logic:
client.on('odds:changed', payload => { try { expensiveAnalytics(payload) } catch (err) { reportToTelemetry(err) }})This keeps a single bad listener from drowning telemetry on every tick.
What “incompatible protocol” looks like
Section titled “What “incompatible protocol” looks like”If you upgrade realtimeodds past a major version where the gateway is older (or vice versa), the gateway may reject the handshake with a protocol mismatch. The SDK surfaces this as a fatal error with a descriptive message. Pin both sides to the same minor track during alpha — see Stability for the version policy.
Listener-removal pitfalls
Section titled “Listener-removal pitfalls”client.off(event, listener) requires the same reference you passed to on. This won’t work:
// BAD — bound function is a new reference each timeclient.on('odds:changed', payload => handle(payload))client.off('odds:changed', payload => handle(payload)) // doesn't remove anythingSave the reference:
const handler = payload => handle(payload)client.on('odds:changed', handler)client.off('odds:changed', handler)