> Fetch clean Markdown by appending `.md` to any page URL under https://signalwire.com/docs or requesting it with the HTTP header `Accept: text/markdown`. The root index at https://signalwire.com/docs/llms.txt lists the available documentation indexes. # Troubleshooting & FAQ Common failure modes and how to diagnose them. For end-to-end patterns, see the guides; for full API contracts, see the [reference](/docs/browser-sdk/v4/reference). ## Connection issues ### `InvalidCredentialsError` on connect **Cause:** Token is expired, malformed, or minted for a different SignalWire space. **Fix:** 1. Mint a fresh SAT from your backend — see [Authentication](/docs/browser-sdk/v4/guides/authentication). 2. Confirm the token was issued by the space you're connecting to. 3. Check that the full token string was copied (SATs are long; truncation is common). ```js import { jwtDecode } from "jwt-decode"; const decoded = jwtDecode(token); const expiresAt = new Date(decoded.exp * 1000); console.log("Expired?", expiresAt < new Date()); ``` ### Connection fails without an obvious error Errors that happen outside of an `await` flow surface on [`errors$`](/docs/browser-sdk/v4/reference/signalwire/errors\$). If you never subscribe to it, those errors are silently dropped — subscribe during client construction so they always reach your logs. ```js client.errors$.subscribe((error) => console.error("Client error:", error)); ``` ### `NotConnectedError` when calling `dial()` **Cause:** Calling [`dial()`](/docs/browser-sdk/v4/reference/signalwire/dial) before the client finished connecting. Wait for [`ready$`](/docs/browser-sdk/v4/reference/signalwire/ready\$) to emit `true`: ```js import { filter, take } from "rxjs"; client.ready$.pipe(filter(Boolean), take(1)).subscribe(async () => { const call = await client.dial(destination); }); ``` ### WebSocket disconnects frequently **Causes:** Unstable network, corporate firewall/proxy blocking WebSocket, idle timeout. The SDK reconnects automatically. Drive a "reconnecting" banner off [`isConnected$`](/docs/browser-sdk/v4/reference/signalwire/is-connected\$): ```js client.isConnected$.subscribe((connected) => { connected ? hideReconnectingBanner() : showReconnectingBanner(); }); ``` ## Video / Audio issues ### Video is black **Causes:** Camera permissions denied, camera in use by another app, wrong camera selected, hardware issue. A denied or unavailable camera no longer fails the call: the SDK falls back to receive-only, so the call connects, remote video arrives, and only the local preview is empty. The reason is reported on `errors$` as a non-fatal [`MediaAccessError`](/docs/browser-sdk/v4/reference/errors/media-access-error) — check there first, since nothing throws: ```js import { MediaAccessError } from "@signalwire/js"; call.errors$.subscribe(({ error }) => { if (error instanceof MediaAccessError) { console.log(error.media, error.denied ? "was denied" : "failed to open"); } }); const permission = await navigator.permissions.query({ name: "camera" }); console.log("Camera permission:", permission.state); client.videoInputDevices$.subscribe((devices) => { console.log("Available cameras:", devices); }); ``` Pass `fallbackToReceiveOnly: false` to `dial()`/`answer()` if you would rather the call fail outright than connect without local media. ### No remote audio **Causes:** Output device not selected, remote participant muted, browser blocking unmuted autoplay, or the `muted` attribute left on the `