diff --git a/packages/sdk-js/src/v2/live/session.test.ts b/packages/sdk-js/src/v2/live/session.test.ts index 18cfd9d..90c00c6 100644 --- a/packages/sdk-js/src/v2/live/session.test.ts +++ b/packages/sdk-js/src/v2/live/session.test.ts @@ -165,4 +165,43 @@ describe('LiveV2Session connectSession', () => { ) expect(session.sessionId).toBe('created-session-id') }) + + it('surfaces init failure without an unhandled rejection', async () => { + const initError = new Error('init failed') + mockHttpPost.mockRejectedValue(initError) + + const unhandledRejections: unknown[] = [] + const onUnhandledRejection = (reason: unknown) => { + unhandledRejections.push(reason) + } + process.on('unhandledRejection', onUnhandledRejection) + + try { + const errorSpy = vi.fn() + const endedSpy = vi.fn() + + const session = new LiveV2Session({ + options: { sample_rate: 16000 }, + httpClient, + webSocketClient, + }) + + session.on('error', errorSpy) + session.on('ended', endedSpy) + + await expect(session.getSessionId()).rejects.toThrow('init failed') + await tick() + + expect(errorSpy).toHaveBeenCalledWith(initError) + expect(endedSpy).toHaveBeenCalledWith({ + code: 1006, + reason: `Couldn't start a new session`, + }) + expect(session.status).toBe('ended') + expect(mockCreateSession).not.toHaveBeenCalled() + expect(unhandledRejections).toEqual([]) + } finally { + process.off('unhandledRejection', onUnhandledRejection) + } + }) }) diff --git a/packages/sdk-js/src/v2/live/session.ts b/packages/sdk-js/src/v2/live/session.ts index 985cd70..1c62dab 100644 --- a/packages/sdk-js/src/v2/live/session.ts +++ b/packages/sdk-js/src/v2/live/session.ts @@ -172,28 +172,44 @@ export class LiveV2Session { } private async startSession(): Promise { - const session = await this.initSessionPromise - this.initSessionResponse = session + try { + const session = await this.initSessionPromise + this.initSessionResponse = session - if (this.abortController.signal.aborted) { - return - } + if (this.abortController.signal.aborted) { + return + } - if (this._status === 'starting') { - this._status = 'started' - this.emit('started', session) - } + if (this._status === 'starting') { + this._status = 'started' + this.emit('started', session) + } - if (this.sessionOptions.messages_config?.receive_lifecycle_events) { - const startSessionMessage: LiveV2StartSessionMessage = { - type: 'start_session', - session_id: session.id, - created_at: session.created_at, + if (this.sessionOptions.messages_config?.receive_lifecycle_events) { + const startSessionMessage: LiveV2StartSessionMessage = { + type: 'start_session', + session_id: session.id, + created_at: session.created_at, + } + this.emit('message', startSessionMessage) } - this.emit('message', startSessionMessage) - } - this.connectToWebSocket(session) + this.connectToWebSocket(session) + } catch (error) { + // initSession already emits 'error' and destroys on failure. + // Catch here so the constructor's fire-and-forget call cannot become an + // unhandled rejection that crashes the Node process. + if (this.abortController.signal.aborted || this._status === 'ended') { + return + } + this.emit( + 'error', + error instanceof Error + ? error + : new Error(`Error starting session: ${error}`, { cause: error }) + ) + this.doDestroy(1006, `Couldn't start session`) + } } private connectToWebSocket(session: LiveV2InitResponse): void {