> ## Documentation Index
> Fetch the complete documentation index at: https://docs.getflowpilot.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Error handling

> How resolve and present failures surface in the Expo SDK, the error codes, and the resilient path to use for must-not-fail flows.

Showing a flow depends on the network, your configuration, and a valid flow schema, so it can fail. FlowPilot is built so a failure never crashes your app or leaves a blank screen, but you still decide what the user sees when there is no flow. This page covers the error type, the codes, and where errors actually surface.

## Where errors surface

The Expo SDK surfaces errors in predictable places, depending on which API you call:

1. **As a `null` session (recommended).** `resolveSession(key)` never throws for predictable failures (network error, missing placement, no flow). It returns `null` when nothing is presentable, so you render your own UI instead.
2. **As a rejected promise.** `createSession(key)` rejects when no presentable flow exists. Wrap it in `try/catch`, or use `resolveSession`.
3. **From the presenter's `onError`.** `<FlowPilotPresenter onError={...} />` fires once if a presentation fails after a session was handed to it (for example a navigation dead-end caught by the watchdog). Pass a `fallback` to render host UI in that case, and `onComplete` still fires with an `error` outcome.
4. **Thrown synchronously from `configure`.** An invalid API key or empty App ID throws immediately from `FlowPilot.configure(...)`.

There is **no error callback** in this build (the SDK does not expose a `setErrorCallback`). Handle errors through the `null`/thrown resolve results and the presenter props above.

<Warning>
  The deprecated `FlowPilot.presentPlacement()` does not render on the Expo SDK: it returns an `error` result immediately without presenting. Resolve with `resolveSession` / `createSession` and render `<FlowPilotPresenter />` instead. See [Presenting placements](/expo-sdk/presenting-placements#deprecated-presentplacement).
</Warning>

## The error type

Errors the SDK produces are a `FlowPilotError`, an `Error` subclass with a typed `code`:

```ts theme={null}
class FlowPilotError extends Error {
  readonly code: ErrorCode;
  readonly underlyingError?: Error;
  readonly context?: Record<string, string>;
}
```

A rejection from `createSession` (or a presenter `onError`) is typed as a plain `Error`, so narrow before reading `code`:

```ts theme={null}
import { FlowPilot, FlowPilotError } from '@flowpilotjs/react-native-sdk';

try {
  const session = await FlowPilot.createSession('paywall_main');
  // render <FlowPilotPresenter session={session} />
} catch (err) {
  if (err instanceof FlowPilotError) {
    console.warn('FlowPilot error:', err.code, err.message);
  } else {
    console.warn('FlowPilot error:', (err as Error)?.message);
  }
}
```

## Error codes

`ErrorCode` is a string union. Each value is the literal string you match against (`err.code === 'NETWORK_ERROR'`):

| Code                         | Category      | Meaning                                                                                         |
| ---------------------------- | ------------- | ----------------------------------------------------------------------------------------------- |
| `INVALID_API_KEY`            | Configuration | API key missing or not prefixed `fp_`.                                                          |
| `SDK_NOT_INITIALIZED`        | Configuration | A present or resolve before `configure(...)`.                                                   |
| `NETWORK_ERROR`              | Network       | The resolve/event request failed at the transport layer.                                        |
| `API_ERROR`                  | Network       | The backend returned a non-2xx status.                                                          |
| `TIMEOUT`                    | Network       | A resolve exceeded `resolveTimeout` and nothing was cached.                                     |
| `RATE_LIMITED`               | Network       | The backend rate-limited the request.                                                           |
| `PLACEMENT_NOT_FOUND`        | Resolution    | The placement id does not exist.                                                                |
| `FLOW_NOT_FOUND`             | Resolution    | The placement resolved but no flow matched (no published flow, or targeting excluded the user). |
| `UNSUPPORTED_SCHEMA_VERSION` | Schema        | The flow uses a newer major schema than this SDK supports.                                      |
| `INVALID_FLOW_SCHEMA`        | Schema        | The resolved flow has no presentable screen.                                                    |
| `COMPONENT_RENDER_ERROR`     | Rendering     | A component failed to render.                                                                   |
| `CUSTOM_COMPONENT_NOT_FOUND` | Rendering     | A flow referenced a custom component that was not registered.                                   |
| `NAVIGATION_ERROR`           | Runtime       | Navigation hit an invalid target.                                                               |
| `VARIABLE_ERROR`             | Runtime       | A variable operation failed.                                                                    |
| `ACTION_ERROR`               | Runtime       | An action failed.                                                                               |
| `ACTION_CHAIN_TIMEOUT`       | Runtime       | An action chain exceeded its timeout.                                                           |
| `INTERNAL_ERROR`             | Internal      | An unexpected internal failure.                                                                 |

A few worth calling out:

* `FLOW_NOT_FOUND` is a **normal, expected** outcome (no flow for this user), not a bug. Show your own UI or nothing.
* `PLACEMENT_NOT_FOUND` is a configuration mistake; check the id against the dashboard.
* `TIMEOUT` only reaches you when the resolve exceeded `resolveTimeout` **and** there was nothing cached or bundled to fall back to.

<Note>
  `configure(...)` validation throws a plain `Error` (message `Invalid API key. Must start with "fp_".` or `App ID is required.`), not a `FlowPilotError`. The `SDK_NOT_INITIALIZED` and resolution codes above come from the present/resolve path.
</Note>

## The resilient path

For anything user-facing, do not depend on catching the right code. Let the SDK walk its fail-safe chain and fall back to your own UI:

* **`resolveSession(key)`** returns a ready session or `null`, never throws. Render your native UI when it is `null`.
* **`<FlowPilotPresenter fallback={...} />`** renders your fallback if a presentation fails before any screen shows.

```tsx theme={null}
const session = await FlowPilot.resolveSession('onboarding');
return session
  ? <FlowPilotPresenter session={session} onComplete={handleComplete} />
  : <MyNativeOnboarding />;
```

With this, a network failure cannot block onboarding: the user gets a cached flow, a bundled default, or your own screen. See [Caching](/expo-sdk/caching) and [Offline and bundled flows](/expo-sdk/offline-bundled-flows).

## Example: branch on the code

If you use the throwing `createSession` API, treat configuration errors differently from transient ones:

```ts theme={null}
import { FlowPilot, FlowPilotError } from '@flowpilotjs/react-native-sdk';

try {
  const session = await FlowPilot.createSession('onboarding');
  // render with <FlowPilotPresenter session={session} /> (the presenter starts it)
} catch (e) {
  const code = e instanceof FlowPilotError ? e.code : undefined;
  switch (code) {
    case 'FLOW_NOT_FOUND':
      // Expected: no flow for this user. Show your own UI or nothing.
      showNativeOnboarding();
      break;
    case 'PLACEMENT_NOT_FOUND':
    case 'INVALID_API_KEY':
    case 'SDK_NOT_INITIALIZED':
      // Configuration problem; needs a code or dashboard fix.
      console.error('FlowPilot config error', e);
      showNativeOnboarding();
      break;
    case 'TIMEOUT':
    case 'NETWORK_ERROR':
    case 'RATE_LIMITED':
      // Transient. Fall back now; the next launch may succeed.
      showNativeOnboarding();
      break;
    default:
      showNativeOnboarding();
  }
}
```

The cleaner option is `resolveSession`, which does this fallback for you.

## Common mistakes

* **Reading `result.error.code` without narrowing.** `result.error` is typed `Error`. Use `instanceof FlowPilotError` before reading `code`.
* **Treating `FLOW_NOT_FOUND` as a crash.** It is a normal no-flow result. Always have something to show.
* **Surfacing raw `message` to users.** It is for your logs. Map codes to user-facing behavior.
* **Using `createSession` on a critical path with no fallback.** It rejects when there is no flow. Use `resolveSession` for must-not-fail entry points.
* **Looking for an error callback.** There is none. Use the `error` outcome and thrown errors.

## Troubleshooting

| Code                         | Likely cause                                                   | Fix                                                                                                         |
| ---------------------------- | -------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `INVALID_API_KEY`            | Key missing or not `fp_`-prefixed                              | Fix the key in [Configuration](/expo-sdk/configuration).                                                    |
| `SDK_NOT_INITIALIZED`        | A call before `configure(...)`                                 | Configure once at launch, before any present/resolve.                                                       |
| `PLACEMENT_NOT_FOUND`        | Wrong placement id                                             | Match the id to the dashboard exactly.                                                                      |
| `FLOW_NOT_FOUND`             | No published flow, or targeting excluded the user              | Expected. Publish and attach a flow, or show your own UI.                                                   |
| `TIMEOUT` / `NETWORK_ERROR`  | Slow/no network and nothing cached                             | Use `resolveSession`, prefetch at launch, ship a bundled default.                                           |
| `UNSUPPORTED_SCHEMA_VERSION` | Flow uses a newer major schema than the SDK                    | Update the app's SDK version.                                                                               |
| `CUSTOM_COMPONENT_NOT_FOUND` | A custom component was not registered, or key/version mismatch | Register the exact key and version before presenting. See [Custom components](/expo-sdk/custom-components). |

## Internal error reporting

When the SDK hits one of its **own** internal failures — a flow that fails to resolve after every fallback, a schema it can't decode, or a component that throws while rendering — it sends a small scoped report to FlowPilot so we can find and fix it. This is **not** a crash reporter:

* It installs **no** global error handlers and never intercepts your app's crashes. It only forwards errors the SDK itself caught.
* It does not touch or conflict with your own Sentry / Crashlytics.
* It is fire-and-forget and best-effort: it never throws, blocks, retries aggressively, or changes what the user sees. Reports are de-duplicated and capped per app launch.

**What's sent:** the error code and message, the SDK version, the platform / OS version, the anonymous SDK user and session ids, and flow / placement identifiers for context. No end-user PII beyond the anonymous ids the SDK already generates.

**Opt out:** set `disableErrorReporting: true` in your configuration and nothing is sent.

```ts theme={null}
FlowPilot.configure({
  apiKey: "fp_...",
  appId: "app_rn_main",
  disableErrorReporting: true, // no internal error reports leave the device
})
```

## Related pages

* [Configuration](/expo-sdk/configuration)
* [Presenting placements](/expo-sdk/presenting-placements)
* [Results and outcomes](/expo-sdk/results-and-outcomes)
* [Offline and bundled flows](/expo-sdk/offline-bundled-flows)
* [Caching](/expo-sdk/caching)
* [Troubleshooting](/expo-sdk/troubleshooting)
