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 three predictable places, depending on which API you call:
- As an
error outcome (imperative). presentPlacement(key) never rejects for predictable failures. It resolves with { outcome: 'error', error }, where error is an Error (a FlowPilotError when the SDK produced it).
- As a rejected promise (declarative).
createSession(key) rejects when no presentable flow exists. Wrap it in try/catch or use resolveSession.
- 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 outcomes and thrown errors above.
The error type
Errors the SDK produces are a FlowPilotError, an Error subclass with a typed code:
class FlowPilotError extends Error {
readonly code: ErrorCode;
readonly underlyingError?: Error;
readonly context?: Record<string, string>;
}
Because presentPlacement’s error is typed as a plain Error, narrow before reading code:
import { FlowPilot, FlowPilotError } from '@flowpilotjs/react-native-sdk';
const result = await FlowPilot.presentPlacement('paywall_main');
if (result.outcome === 'error') {
const err = result.error;
if (err instanceof FlowPilotError) {
console.warn('FlowPilot error:', err.code, err.message);
} else {
console.warn('FlowPilot error:', err?.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.
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.
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.
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 and Offline and bundled flows.
Example: branch on the code
If you use the throwing/imperative APIs, treat configuration errors differently from transient ones:
import { FlowPilot, FlowPilotError } from '@flowpilotjs/react-native-sdk';
try {
const session = await FlowPilot.createSession('onboarding');
session.start();
// render with <FlowPilotPresenter />
} 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. |
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. |
Related pages