Skip to main content

WebRTC JS SDK Error Handling

This guide reflects the current SDK surface in this branch. The current SDK exposes error-related behavior through three main channels:
  • telnyx.error for structured failures.
  • telnyx.warning for degraded but recoverable conditions.
  • telnyx.notification for call/session updates plus a small set of backward-compatible notifications.
Use telnyx.ready to know when the client is authenticated and the gateway is ready. Do not treat readiness as a telnyx.notification case.

Table of Contents

Event Overview

EventPurposeRecommended use
telnyx.readyClient is authenticated and gateway reached REGISTER or REGEDEnable calling UI and flush any reconnect state
telnyx.errorFatal or blocking SDK errorsShow actionable errors, retry, re-authenticate, or fail the current action
telnyx.warningNon-fatal quality, connectivity, and token warningsShow degraded-state UI and collect telemetry
telnyx.notificationCall lifecycle updates and compatibility notificationsDrive call UI and hangup handling
telnyx.socket.closeRaw WebSocket close eventLog close codes and monitor reconnect behavior
telnyx.socket.errorRaw WebSocket error wrapperLog opaque socket failures alongside sessionId

Structured Errors (telnyx.error)

telnyx.error is the primary error surface in the current SDK.

Imports

import {
  SwEvent,
  TelnyxError,
  TELNYX_ERROR_CODES,
  SDK_ERRORS,
  isMediaRecoveryErrorEvent,
} from '@telnyx/webrtc';
  • TelnyxError is the structured error class emitted by the SDK.
  • isMediaRecoveryErrorEvent() is the type guard for inbound permission recovery flows.
  • TELNYX_ERROR_CODES provides named constants for numeric error comparisons.
  • SDK_ERRORS provides the registered error metadata.

Payload shapes

interface ITelnyxStandardErrorEvent {
  error: ITelnyxError;
  sessionId: string;
  callId?: string;
  recoverable?: false;
}

interface ITelnyxMediaRecoveryErrorEvent {
  error: ITelnyxMediaError;
  sessionId: string;
  callId: string;
  recoverable: true;
  retryDeadline: number;
  resume: () => void;
  reject: () => void;
}

type ITelnyxErrorEvent =
  | ITelnyxStandardErrorEvent
  | ITelnyxMediaRecoveryErrorEvent;
recoverable: true is used only for inbound media-permission recovery when mediaPermissionsRecovery.enabled is configured and the initial getUserMedia() attempt fails while answering a call.

Basic example

client.on(SwEvent.Error, (event) => {
  if (isMediaRecoveryErrorEvent(event)) {
    openPermissionsDialog({
      deadline: event.retryDeadline,
      onRetry: () => event.resume(),
      onCancel: () => event.reject(),
    });
    return;
  }

  if (!(event.error instanceof TelnyxError)) {
    showErrorMessage('An unknown SDK error occurred.');
    return;
  }

  switch (event.error.code) {
    case TELNYX_ERROR_CODES.NETWORK_OFFLINE:
      showErrorMessage('You appear to be offline.');
      break;
    case TELNYX_ERROR_CODES.AUTHENTICATION_REQUIRED:
      showErrorMessage('Session expired. Please authenticate again.');
      break;
    default:
      showErrorMessage(event.error.message);
  }
});

Error code reference

SDP errors

CodeNameMessageTypical trigger
40001SDP_CREATE_OFFER_FAILEDFailed to create call offerRTCPeerConnection.createOffer() failed
40002SDP_CREATE_ANSWER_FAILEDFailed to answer the callRTCPeerConnection.createAnswer() failed
40003SDP_SET_LOCAL_DESCRIPTION_FAILEDFailed to apply local call settingssetLocalDescription() failed
40004SDP_SET_REMOTE_DESCRIPTION_FAILEDFailed to apply remote call settingssetRemoteDescription() failed
40005SDP_SEND_FAILEDFailed to send call data to serverInvite/answer signaling could not be sent

Media errors

CodeNameMessageTypical trigger
42001MEDIA_MICROPHONE_PERMISSION_DENIEDMicrophone access deniedBrowser or OS denied microphone permission
42002MEDIA_DEVICE_NOT_FOUNDNo microphone foundMissing/disconnected device or invalid deviceId
42003MEDIA_GET_USER_MEDIA_FAILEDFailed to access microphonegetUserMedia() failed for another reason

Call-control errors

CodeNameMessageTypical trigger
44001HOLD_FAILEDFailed to hold the callHold request failed
44002INVALID_CALL_PARAMETERSInvalid call parametersRequired call params were missing or invalid
44003BYE_SEND_FAILEDFailed to hang up cleanlyLocal hangup succeeded but BYE could not be sent
44004SUBSCRIBE_FAILEDFailed to subscribe to call eventsVerto subscribe failed

WebSocket and transport errors

CodeNameMessageTypical trigger
45001WEBSOCKET_CONNECTION_FAILEDUnable to connect to serverWebSocket construction/connection failed
45002WEBSOCKET_ERRORConnection to server lostBrowser ws.onerror fired
45003RECONNECTION_EXHAUSTEDUnable to reconnect to serverGateway reconnect attempts were exhausted
45004GATEWAY_FAILEDGateway connection failedGateway reported FAILED or FAIL_WAIT

Authentication and session errors

CodeNameMessageTypical trigger
46001LOGIN_FAILEDAuthentication failedLogin rejected or registration never reached ready state
46002INVALID_CREDENTIALSInvalid credential parametersClient-side login validation failed before request send
46003AUTHENTICATION_REQUIREDAuthentication requiredRequest sent before auth completed or after auth was lost
48001NETWORK_OFFLINEDevice is offlineBrowser offline event fired
49001UNEXPECTED_ERRORAn unexpected error occurredUnclassified failure during peer/call setup
[!NOTE] Invalid login options currently also include type: ERROR_TYPE.invalidCredentialsOptions on the runtime event payload for backward compatibility. The stable signal to key on is still event.error.code === TELNYX_ERROR_CODES.INVALID_CREDENTIALS.

Structured Warnings (telnyx.warning)

Warnings are not fatal. They describe degraded behavior, quality issues, or situations that may need user action before the session breaks.

Payload shape

interface ITelnyxWarningEvent {
  warning: ITelnyxWarning;
  sessionId: string;
  callId?: string;
}

Basic example

import { SwEvent, TELNYX_WARNING_CODES, SDK_WARNINGS } from '@telnyx/webrtc';

client.on(SwEvent.Warning, ({ warning, callId }) => {
  if (warning.code === TELNYX_WARNING_CODES.TOKEN_EXPIRING_SOON) {
    refreshTokenSoon();
    return;
  }

  if (warning.code === TELNYX_WARNING_CODES.PEER_CONNECTION_FAILED) {
    showWarningBanner(`Call ${callId ?? ''} is reconnecting`.trim());
    return;
  }

  console.debug(SDK_WARNINGS[warning.code]?.description);
  console.warn(`[${warning.code}] ${warning.name}: ${warning.message}`);
});

Warning code reference

Network quality warnings

CodeNameMessageTypical trigger
31001HIGH_RTTHigh network latency detectedRTT stayed above threshold
31002HIGH_JITTERHigh jitter detectedJitter stayed above threshold
31003HIGH_PACKET_LOSSHigh packet loss detectedPacket loss stayed above threshold
31004LOW_MOSLow call quality scoreMOS stayed below threshold

Data-flow warnings

CodeNameMessageTypical trigger
32001LOW_BYTES_RECEIVEDNo audio data receivedRemote audio bytes stopped increasing
32002LOW_BYTES_SENTNo audio data being sentLocal audio bytes stopped increasing

Connectivity warnings

CodeNameMessageTypical trigger
33001ICE_CONNECTIVITY_LOSTConnection interruptedICE connection state became disconnected
33002ICE_GATHERING_TIMEOUTICE gathering timed outICE gathering safety timeout fired
33003ICE_GATHERING_EMPTYNo ICE candidates gatheredNo candidates were collected
33004PEER_CONNECTION_FAILEDConnection failedPeer connection state became failed
33005ONLY_HOST_ICE_CANDIDATESOnly local network candidates availableSDP contained only host ICE candidates

Authentication and session warnings

CodeNameMessageTypical trigger
34001TOKEN_EXPIRING_SOONAuthentication token expiring soonJWT expires within 120 seconds
35001SESSION_NOT_REATTACHEDActive call lost after reconnectServer returned an empty reattached_sessions list while calls still existed locally

Notifications (telnyx.notification)

telnyx.notification is still important, but it is no longer the main place to integrate error handling logic. Use it primarily for call lifecycle and UI synchronization.

Important clarification

  • telnyx.ready is separate from telnyx.notification.
  • The vertoClientReady payload is emitted on telnyx.ready, not on telnyx.notification.
notification.typeCurrent statusPayload notes
callUpdateActiveIncludes call; use this for call state and hangup data
userMediaErrorCompatibility notificationFinal media-failure notification. Includes raw browser/media error, errorName, errorMessage, and call when available
peerConnectionFailureErrorCompatibility notificationIncludes raw error; structured replacement is warning 33004
signalingStateClosedActiveIncludes sessionId; indicates the current peer signaling state closed

Listener scoping

Notifications are dispatched to the call scope first. If a call-level listener handles the notification, the session-level listener does not also receive it — it is one or the other, not both.
  • onNotification on a call (passed via call options) is the highest-priority hook for that call.
  • client.on(SwEvent.Notification, ...) is the session-level fallback that only fires when no call-level listener is registered.
  • If no listeners exist at either level, the notification is silently dropped.
For peerConnectionFailureError and signalingStateClosed, prefer call-scoped handling or the structured warning/error events when you need explicit call correlation. The compatibility notification payload itself does not include callId.

Screen-share behavior

All telnyx.notification dispatches are suppressed for calls created with screenShare: true. If you need recovery state for a screen-share call, inspect the call object directly:
if (screenShareCall.signalingStateClosed) {
  console.log('This screen-share peer is no longer recoverable.');
}

Example: hangup and recovery-aware call updates

client.on(SwEvent.Notification, (notification) => {
  if (notification.type !== 'callUpdate' || !notification.call) {
    return;
  }

  const call = notification.call;

  if (call.recoveredCallId) {
    removeOldCallUi(call.recoveredCallId);
  }

  if (call.state === 'recovering') {
    showConnectionStatus('recovering');
    return;
  }

  if (call.state === 'hangup') {
    console.log('Call ended', {
      cause: call.cause,
      causeCode: call.causeCode,
      sipCode: call.sipCode,
      sipReason: call.sipReason,
    });
  }
});

Call Termination Data

When a call reaches hangup, inspect these fields on the Call object:
FieldTypeMeaning
causestringHigh-level cause such as USER_BUSY or CALL_REJECTED
causeCodenumberNumeric cause code
sipCodenumberSIP response code when available
sipReasonstringSIP reason phrase when available
Typical cases:
Field valueMeaning
cause === 'NORMAL_CLEARING'Expected call completion
cause === 'USER_BUSY'Remote party was busy
cause === 'CALL_REJECTED'Remote party rejected the call
cause === 'NO_ANSWER'Call timed out unanswered
cause === 'UNALLOCATED_NUMBER'Dialed number is invalid or does not exist
cause === 'PURGE'Call was purged from the system
sipCode === 403Forbidden
sipCode === 404Destination not found
sipCode === 486Busy Here

Socket Events

The SDK exposes both raw socket events and structured transport errors.

telnyx.socket.close

telnyx.socket.close delivers the browser CloseEvent unchanged. During a forced safety cleanup, the SDK emits a synthetic abnormal close with:
  • code: 1006
  • reason: 'STUCK_WS_TIMEOUT: Socket got stuck in CLOSING state and was forcefully cleaned up by safety timeout'
  • wasClean: false
Useful close codes:
CodeMeaning
1000Normal closure
1001Going away
1002Protocol error
1003Unsupported data
1005No status code received
1006Abnormal closure
1011Internal error

telnyx.socket.error

telnyx.socket.error delivers:
{
  error: ErrorEvent | Event;
  sessionId: string;
}
This event is intentionally low-detail because browsers expose very little information for WebSocket errors. The SDK also emits telnyx.error with code 45002 (WEBSOCKET_ERROR) when ws.onerror fires.

Connection state helpers

The browser session exposes WebSocket state helpers on client.connection:
GetterMeaning
client.connection.connectingWebSocket is in CONNECTING
client.connection.connectedWebSocket is in OPEN
client.connection.closingWebSocket is in CLOSING
client.connection.closedWebSocket is in CLOSED
client.connection.isAliveCONNECTING or OPEN
client.connection.isDeadCLOSING or CLOSED
Example:
const placeCall = (destinationNumber: string) => {
  if (!client.connection.connected) {
    showErrorMessage('Still connecting to Telnyx. Please try again shortly.');
    return;
  }

  client.newCall({ destinationNumber });
};

Reconnection Behavior

The previous version of this document described a generic exponential-backoff flow. That is not how the current browser SDK reconnects.

What the current SDK does

  1. On telnyx.socket.close or telnyx.socket.error, the SDK clears subscriptions and resets gateway readiness state.
  2. If autoReconnect is enabled, the browser session schedules connect() after client.reconnectDelay.
  3. In the browser session, reconnectDelay is currently 1000 ms.
  4. When the gateway reports REGISTER or REGED again, the SDK emits telnyx.ready again.

Gateway retry behavior

Gateway-state retries use separate jittered delays:
  • UNREGED / NOREG: up to 5 registration retries, each delayed by a random 2 to 6 seconds. After that the SDK emits LOGIN_FAILED (46001).
  • FAILED / FAIL_WAIT: GATEWAY_FAILED (45004) is emitted on first detection. If autoReconnect stays enabled, the SDK retries up to 5 times with a random 2 to 6 second delay before RECONNECTION_EXHAUSTED (45003).

Keeping media alive across socket loss

If keepConnectionAliveOnSocketClose is true, the SDK will try to preserve active peer connections while signaling reconnects.
  • If the peer is still recoverable, the SDK disconnects and reconnects the socket while keeping the call alive.
  • If the peer signaling state is already closed, the SDK falls back to a full reconnect path.

Recovery and call objects

Recovery can create a new Call object. When that happens:
  • The new call exposes recoveredCallId.
  • The call may stay in recovering until media/signaling are restored.
  • Your UI should remove or merge the old call UI using recoveredCallId.

Legacy RTC Events and Migration

The SDK still exposes low-level RTC events, but new integrations should prefer telnyx.error, telnyx.warning, and telnyx.notification.

Legacy or low-level RTC events

EventStatusPreferred surface
telnyx.rtc.mediaErrorLegacy/compatibilitytelnyx.error with 42001, 42002, or 42003
telnyx.rtc.peerConnectionFailureErrorLegacy/compatibilitytelnyx.warning with 33004
telnyx.rtc.peerConnectionSignalingStateClosedLow-level active eventnotification.type === 'signalingStateClosed' if you want the higher-level notification

Migration checklist

  1. Add a telnyx.error listener and switch on event.error.code.
  2. Add a telnyx.warning listener and switch on warning.code.
  3. Keep telnyx.notification for callUpdate and any compatibility notifications you still depend on.
  4. Treat telnyx.ready as the only readiness signal.
  5. Prefer TELNYX_ERROR_CODES and TELNYX_WARNING_CODES over hard-coded numeric literals.
  6. If you support inbound permission recovery, enable mediaPermissionsRecovery and handle isMediaRecoveryErrorEvent(event).