Two booleans tell you everything you need: whether the session is over, and whether retrying would help.
All SDK errors follow this shape — surfaced via the error state from useAvatar and the error event from useAvatarEvent.
fatal is about the call, not the avatarThese are two separate questions, and an error only answers the first one:
fatal — is the call over?isReady (from useAvatar) — is there still an avatar on screen?The same code lands differently depending on when it fires. A connection failure during a cold start leaves an empty frame; the identical failure ten seconds into a call leaves the avatar exactly where it was. So code can't tell you what to render, and neither can fatal — isReady can:
| | isReady | what to do |
|---|---|---|
| fatal | true | Avatar's still there. Toast the reason; your start control is the retry. |
| fatal | false | Nothing to show — this is what errorFallback is for. |
| not fatal | — | The call is still live. Toast at most. |
The good news is you rarely write that table yourself: <Avatar> reads it for you (see below).
fatal — is the session over?
true — the call is over, and the SDK has already cleaned it up. The avatar stays on screen; start() opens a new call. Tell them why, and let your existing start control be the retry.false — the session is still live. One specific thing failed (a single turn, one dropped speakText) and the call continues underneath. Show a transient hint, or ignore it.retryable — would start() plausibly succeed? Only meaningful when fatal. Every fatal error is retryable except MIC_PERMISSION_DENIED, where the user must change a browser setting first. Use it to decide whether to render a Retry button, so you never show one that can't work.
The SDK performs the teardown off fatal itself, so it can never tell you the session survived when it didn't. Read fatal and trust it.
error is sticky. It stays set until the next start() (specifically, until the session reaches connecting) and never clears on a timer. You can render a dead-end screen off error?.fatal and trust it to stay put.
Codes are grouped by who fixes it. There are fewer of them than you might expect, because a code only exists where the fix differs. If you need to know exactly which call failed, that detail is in message and cause.
You fix these in your own code. All are non-fatal except the token failure — they report a dropped call, not a broken session.
| Code | Fatal | When it fires |
|---|---|---|
SESSION_TOKEN_FETCH_FAILED | Yes | Your getSessionToken() rejected or returned nothing. cause holds the error your backend threw. Fires from both the load and the connect path. |
INVALID_MODE | No | Called a method the current mode doesn't support (speakText() outside tts, speakAudio() outside audio). The session is unaffected. |
INVALID_INPUT | No | Bad argument: empty text, text over 5000 chars, context over 2000, audio over 15MB. That call was dropped; nothing else changed. |
NOT_CONNECTED | No | Called a method needing a live session before start(), or after it ended. That call was dropped. Not a connection failure — see CONNECTION_FAILED. |
| Code | Fatal | When it fires |
|---|---|---|
MIC_PERMISSION_DENIED | Yes | Microphone refused (call mode). The only fatal code with retryable: false — they must grant access in browser settings before start() can succeed. |
Retry, or report to us.
| Code | Fatal | When it fires |
|---|---|---|
CONNECTION_FAILED | Yes | Could not reach or stay connected to the avatar service — failed to open, transport error, or dropped mid-session. Inspect cause. |
SERVER_ERROR | Yes | The service hit an error. message carries its reason. |
SERVER_CLOSE_REQUESTED | Yes | The service ended the session deliberately (expired auth, policy). message carries its reason. |
AVATAR_LOAD_FAILED | Yes | The avatar's config or .riv could not be resolved, fetched, or parsed. Check the avatarId. Nothing is painted when this fires. |
BRAIN_ERROR | No | One turn failed to produce a response. The call keeps running — the user can just speak again. Do not tear your UI down on this. |
| Code | Fatal | When it fires |
|---|---|---|
SESSION_IDLE_TIMEOUT | Yes | The service closed the call after a stretch of silence (call mode). Offer a reconnect. |
UNKNOWN | Varies | Nothing else fit. Inspect message and cause, and please report it to us. |
Usage limits and plan stops are not errors. They arrive on the notice channel so the avatar can speak them before the session closes — drive your upgrade CTA off notice, not off error.
If you do nothing at all, <Avatar> renders a built-in card whenever a fatal error leaves nothing painted, so you never ship a blank frame. Override the copy with errorFallback:
You don't need to know the code list to do this correctly:
A fatal error ends the session, not the avatar. stop() deliberately keeps the avatar painted so restarting is cheap, and isIdle flips back to true — so your existing start control reappears and is the retry. There is usually nothing to rebuild:
errorFallback covers the case with nothing to show. <Avatar> decides that by looking at whether an avatar is actually on screen — not at the code — so it stays right regardless of which error fired or when. AVATAR_LOAD_FAILED is the usual one, but a token failure or a dropped connection before the first render lands there too.
Then tell them why the call ended, in a toast, with copy written for them:
Never render error.message. It is written for you, not your users — "Failed to fetch session token", "speakText() needs a live session". Log it, send it to your error tracker, and write your own copy off code.
Non-fatal errors belong in a toast, not a screen — the call is still running underneath, so replacing the avatar would be a downgrade:
Use useAvatarEvent("error", ...) for analytics and logging — this is where the diagnostic fields earn their keep.
retryable is for deciding whether to show a button, not for retrying by yourself. Reconnecting automatically on SESSION_IDLE_TIMEOUT rebuilds a session that idles out again minutes later, and on SERVER_CLOSE_REQUESTED it walks back into the rate limit that just closed you.
INVALID_MODE, INVALID_INPUT, and NOT_CONNECTED mean your code has a bug, not that anything failed at runtime. They're non-fatal and the session is untouched. Wire this up once and they'll tell you in development:
The session is over and the SDK has already cleaned it up. The avatar stays painted, but nothing will happen until you call start(). Explain what happened, and offer a retry when retryable.
CONNECTION_FAILED, SERVER_ERROR, SERVER_CLOSE_REQUESTED, AVATAR_LOAD_FAILED, MIC_PERMISSION_DENIED, SESSION_IDLE_TIMEOUT, SESSION_TOKEN_FETCH_FAILEDThe call is still live. One turn or one method call failed. Show a toast at most — replacing the avatar here would end a session that was working fine.
BRAIN_ERROR, INVALID_MODE, INVALID_INPUT, NOT_CONNECTED