Skip to main content

Error Handling & Troubleshooting

Every failure surfaced by @video/video-client-core is a VideoClientError. This guide is symptom-first: find the failure you're seeing below, follow the decision tree to the likely cause, and apply the resolution.

How SDK errors work

Async SDK calls (createCall, call.broadcast, createPlayer, media initialization) reject with a VideoClientError. Catch it and branch on error.code:

try {
const call = await createCall({ /* … */ });
} catch (err) {
const error = err as VideoClientError;
switch (error.code) {
case 'token-error':
// token expired or invalid — refresh and retry
break;
case 'connection-error':
// backend unreachable — retry with backoff
break;
default:
console.error(`[${error.severity}] ${error.code}`, error.inner);
}
}

A VideoClientError carries:

FieldMeaning
codeA stable string identifier to branch on (e.g. "token-error", "connection-error").
severity"warn" (recoverable, the call may continue) or "fatal" (the call/player is unusable).
innerThe underlying error (DOM exception, transport error) when one exists. Log this for diagnosis.
toJSON() / log()Serialize or emit the error through the SDK logger.
Branch on code, not on message text

Error messages may change between releases; error.code string values are the stable contract. Always compare against error.code, never against error.message.

ErrorCode is deprecated

The scenario walkthroughs below use the ErrorCode enum (e.g. ErrorCode.TokenError) because it reads clearly in a switch. The enum itself is deprecated in the SDK source — it's kept only for backwards compatibility, and several members alias the same underlying string (for example NoBackendEndpoints, BadInput, and NoUserId all map to "bad-input"). For new code, prefer comparing error.code directly against its string value (as in the snippet above), or use the type-predicate helpers on the errors namespace (e.g. errors.isNetworkError(err)) if you need type-narrowing without a string literal.

The full list of codes lives in the generated ErrorCode reference, and the class shape in VideoClientError.


1. Connection failure

Symptom: createCall or createPlayer rejects immediately or after a few seconds; nothing connects; no media flows.

Likely causes (most frequent first):

  1. backendEndpoints is wrong, empty, or unreachable from the client network.
  2. The backend is up but the transport handshake fails (proxy, firewall, or TLS).
  3. Transient backend unavailability.

Codes: ConnectionError, NoBackendEndpoints, TransportError.

Resolution:

  1. Confirm backendEndpoints is a non-empty array of reachable URLs (NoBackendEndpoints means it was empty/omitted).
  2. From the client machine, verify the endpoint responds (open it in a browser / curl).
  3. Check for a corporate proxy or firewall blocking WebSocket/WebRTC.
  4. On ConnectionError/TransportError, retry with exponential backoff (see code below) — these are often transient.

Prevention: validate backendEndpoints before createCall; wrap connection setup in the retry helper below.

import { ErrorCode, VideoClientError } from '@video/video-client-core';

async function createCallWithRetry(options, maxAttempts = 3) {
let lastError;
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await createCall(options);
} catch (err) {
const error = err as VideoClientError;
lastError = error;
// Only retry transient transport-level failures.
const retryable =
error.code === ErrorCode.ConnectionError ||
error.code === ErrorCode.TransportError ||
error.code === ErrorCode.NetworkError;
if (!retryable || attempt === maxAttempts) throw error;
const backoffMs = 2 ** attempt * 250; // 500ms, 1s, 2s…
await new Promise((resolve) => setTimeout(resolve, backoffMs));
}
}
throw lastError;
}

2. Token expiry

Symptom: A call/player that was working starts failing, or a fresh connection is rejected at auth. Common after a session has been open a while.

Likely causes (most frequent first):

  1. The JWT has expired (past its exp).
  2. The token was minted for a different backend/audience.
  3. The token was revoked server-side.

Codes: TokenError, AuthenticationError.

Resolution:

  1. On TokenError, fetch a fresh token from your auth endpoint and retry the operation (see below).
  2. Confirm your token-minting service sets a sensible lifetime and the right audience/backend claims.
  3. If a freshly minted token still fails, the problem is auth configuration — see scenario 7.

Prevention: refresh tokens proactively before they expire rather than reacting to TokenError; keep clock skew small between the token issuer and clients.

import { ErrorCode, VideoClientError } from '@video/video-client-core';

async function withTokenRefresh(operation, fetchFreshToken) {
try {
return await operation();
} catch (err) {
const error = err as VideoClientError;
if (error.code === ErrorCode.TokenError) {
// Refresh the credential and retry exactly once.
await fetchFreshToken();
return await operation();
}
throw error;
}
}

3. Device permission denied

Symptom: The browser permission prompt is dismissed or blocked; camera/mic never starts; preview stays black.

Likely causes (most frequent first):

  1. The user denied the camera/microphone prompt.
  2. Permissions are blocked at the browser or OS level for the site.
  3. The page is not served over a secure context (HTTPS/localhost), so getUserMedia is unavailable.

Codes: DevicePermissionDenied, PermissionDenied.

Resolution:

  1. On DevicePermissionDenied, show guidance to re-enable the permission in the browser site settings.
  2. Offer a degraded path (audio-only, or view-only) instead of a hard failure (see below).
  3. Confirm the page is on HTTPS or localhostgetUserMedia refuses insecure origins.

Prevention: request permissions in response to a user gesture with clear context; detect a blocked state early and render an explanatory UI rather than a broken preview.

import { ErrorCode, VideoClientError } from '@video/video-client-core';

async function startWithFallback(startVideo, startAudioOnly, showPermissionHelp) {
try {
return await startVideo();
} catch (err) {
const error = err as VideoClientError;
if (
error.code === ErrorCode.DevicePermissionDenied ||
error.code === ErrorCode.PermissionDenied
) {
showPermissionHelp(); // explain how to re-enable in browser settings
return await startAudioOnly(); // degrade instead of failing hard
}
throw error;
}
}

4. Media device not available

Symptom: No camera/mic is offered, or the selected device errors out even though permission was granted.

Likely causes (most frequent first):

  1. No matching device is attached (headless machine, unplugged webcam).
  2. Another application holds the device exclusively.
  3. A previously selected deviceId no longer exists (device was removed).

Codes: MediaDeviceNotAvailable, DeviceNotFound, DeviceInUse.

Resolution:

  1. Enumerate devices and confirm at least one camera/mic is present.
  2. On DeviceInUse, prompt the user to close the other app (Zoom, Photo Booth, another tab).
  3. On DeviceNotFound, fall back to the default device instead of a stale saved deviceId.

Prevention: re-enumerate devices on devicechange; never persist a deviceId without a fallback to the system default.


5. Codec mismatch

Symptom: Connection succeeds but no video renders, playback fails, or the broadcast is rejected on this browser only.

Likely causes (most frequent first):

  1. The browser lacks H.264 support required by the stream.
  2. The negotiated format isn't supported by this player/browser.
  3. A requested quality/format profile isn't available.

Codes: H264NotSupported, PlayerNotSupported, FormatNotFound.

Resolution:

  1. On H264NotSupported, direct users to a browser with H.264 (or enable it in their build).
  2. Detect codec support up front with RTCRtpReceiver.getCapabilities('video') before offering broadcast.
  3. On FormatNotFound, fall back to an available quality profile.

Prevention: feature-detect codec support at load and show a compatibility notice before the user tries to broadcast/play.


6. Network instability

Symptom: The call connects but drops, freezes, or repeatedly reconnects; quality oscillates.

Likely causes (most frequent first):

  1. Intermittent packet loss / bandwidth starvation on the client link.
  2. The signaling WebSocket dropped and is reconnecting.
  3. Transport state churn under a flaky connection.

Codes: NetworkError, WebsocketError, TransportState.

Resolution:

  1. Treat warn-severity NetworkError/TransportState as recoverable — the SDK attempts to recover; surface a "reconnecting…" indicator rather than tearing down the call.
  2. Only tear down on fatal severity.
  3. On persistent WebsocketError, verify no proxy is killing idle WebSocket connections.

Prevention: inspect error.severity and keep the UI responsive during transient blips instead of ending the session.


7. Auth misconfiguration

Symptom: Every attempt to connect fails at auth, including with a freshly minted token — distinct from token expiry, which fails only after a while.

Likely causes (most frequent first):

  1. Wrong authUrl, signing key, or audience/issuer mismatch between the minting service and backend.
  2. Required identity fields missing (e.g. no userId).
  3. The credential lacks permission for the requested resource.

Codes: AuthenticationError, AccessDenied, NoUserId.

Resolution:

  1. On NoUserId, ensure the user.userId is set when creating the call.
  2. On AuthenticationError/AccessDenied, verify the token's issuer, audience, and signing key match the backend's expectations.
  3. Decode the JWT (e.g. jwt.io) and confirm its claims against your backend config.

Prevention: validate the auth round-trip in a staging environment before shipping; assert required identity fields client-side before calling the SDK.


8. Version mismatch

Symptom: A method throws where it used to work after an SDK upgrade, or a documented capability is rejected on this backend.

Likely causes (most frequent first):

  1. Calling an API that is deprecated or removed in the installed SDK version.
  2. Requesting an operation the connected backend doesn't support.
  3. Client and backend versions are incompatible.

Codes: Deprecated, NotSupported, OperationNotSupported.

Resolution:

  1. On Deprecated, check the release notes / changelog for the replacement API.
  2. On OperationNotSupported, confirm the backend supports the feature at its deployed version.
  3. Align the installed @video/video-client-core version with your backend.

Prevention: pin SDK versions, read the changelog before upgrading, and gate new capabilities on backend support.


Next steps