<!-- AvatarFactory Docs · https://avatarfactory.in/docs/sdk/react-native/error-handling · Full map: https://avatarfactory.in/llms.txt -->

# Error Handling

Two booleans tell you everything you need: whether the session is over, and whether retrying would help.

## AvatarError type

All SDK errors follow this shape — surfaced via the `error` state from `useAvatar` and the `error` event from `useAvatarEvent`.

```tsx
type AvatarError = {
  // --- Branch on these ---
  code: AvatarErrorCode;      // What went wrong, in terms of what you can do
  fatal: boolean;             // Is the session over?
  retryable: boolean;         // Would calling start() again help?

  // --- Log these; don't render them ---
  message: string;            // Written for you, not your users
  source: AvatarErrorSource;  // "network" | "server" | "sdk"
  cause?: unknown;            // The underlying JS error, if any
};
```

## `fatal` is about the call, not the avatar

These 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: `` reads it for you (see below).

## The two fields that matter

**`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 grant access in Settings 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.

## Error codes

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`.

### Your integration

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, play() outside player). 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. |

### Your user

| 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 Settings before start() can succeed. |

### The service

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. |

### Lifecycle

| 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`.

## The minimum that works

If you do nothing at all, `` renders a built-in card whenever a fatal error leaves nothing painted, so you never ship a blank frame. Override the copy with `errorFallback`:

```tsx
<Avatar loader={<Spinner />} errorFallback={<CouldNotConnect />} />
```

## Handling via state

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:

```tsx
function CallStage() {
  // No error branch at all. An idle timeout or a dropped connection ends the
  // session and leaves the avatar on screen; the controls' button flips back to
  // "Start" on its own. Replacing this with a dead-end screen would throw away
  // a working avatar AND the control that fixes it.
  return <Avatar errorFallback={<CouldNotLoad />} />;
}
```

`errorFallback` covers the case with nothing to show. `` 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:

```tsx
const COPY: Partial<Record<AvatarErrorCode, string>> = {
  MIC_PERMISSION_DENIED: "Allow microphone access in Settings.",
  SESSION_IDLE_TIMEOUT: "The call ended after a stretch of silence.",
  CONNECTION_FAILED: "We couldn't reach the service. Check your connection.",
};

const body = COPY[error.code] ?? "Something went wrong on our side.";
```

> **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:

```tsx
function TurnHiccupToast() {
  const { error } = useAvatar();
  if (!error || error.fatal) return null;
  // e.g. BRAIN_ERROR — the avatar just missed a turn. Still connected.
  return <Toast>Sorry, I missed that — try again?</Toast>;
}
```

## Handling via events

Use `useAvatarEvent("error", ...)` for analytics and logging — this is where the diagnostic fields earn their keep.

```tsx
function ErrorHandler() {
  useAvatarEvent("error", (error) => {
    // Send the full detail somewhere you can read it later. `message` and
    // `cause` belong HERE, not on screen.
    analytics.track("avatar_error", {
      code: error.code,
      source: error.source,
      fatal: error.fatal,
      message: error.message,
    });
  });

  return null;
}
```

> `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.

## Common patterns

### Microphone denied

When the user denies the mic, the SDK already shows a native alert offering to open Settings. This is the one fatal error where `retryable` is `false` — a Retry button would do nothing until they change the setting, so send them there instead:

```tsx
import { View, Text, Linking, Button } from "react-native";

function MicGate() {
  const { error } = useAvatar();

  if (error?.code === "MIC_PERMISSION_DENIED") {
    return (
      <View>
        <Text>🎤 Microphone access is blocked.</Text>
        <Button title="Open Settings" onPress={() => Linking.openSettings()} />
      </View>
    );
  }

  return null;
}
```

## While you're integrating

`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:

```tsx
useAvatarEvent("error", (err) => {
  if (err.source === "sdk" && !err.fatal) {
    console.warn(`[avatar] ${err.code}: ${err.message}`);
  }
});
```

## Fatal vs non-fatal

    Fatal (fatal: true)
    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_FAILED

    Non-fatal (fatal: false)
    The 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
