The error type
Every failure the SDK surfaces is aFlowPilotError:
isClientError and isServerError read a status_code from context when present. Use them to decide whether a retry could help: a client error (bad config, bad request) will not fix itself, a server error might.
Error codes
FlowPilotErrorCode is a string enum. The full set:
A few worth calling out:
flowNotFoundmeans the placement resolved but no flow matched (no published flow, or targeting and frequency rules excluded the user). This is a normal, expected outcome, not a bug. Show your own UI or nothing.placementNotFoundmeans the placement id does not exist. That is a configuration mistake, so check the id against the dashboard.timeoutmeans the resolve did not finish withinresolveTimeout. The SDK falls back to cache or bundled flows first; you only see a throwntimeoutif nothing was cached.
Where errors actually surface
There are three reliable ways to observe errors today:- Thrown from the throwing APIs.
presentPlacement(_:from:options:)andcreateSession(...)arethrows. Wrap them indo/catchand inspect theFlowPilotError. - As a
.erroroutcome onFlowResult. When a flow ends in error,result.outcome == .errorandresult.errorholds theFlowPilotError. See Results and outcomes. - Never thrown, by design. The fallback overloads below never throw. They return a
FlowResult(with.errorif they had to fall back to your own UI) ornil.
The resilient path
For anything a user sees, prefer the presentation overloads that never throw. They walk the full fallback chain (fresh cache, then a live resolve bounded byresolveTimeout, then stale cache, then a bundled default flow) before giving up:
Example
If you use the throwing API, catch the error and branch on its code. Treat configuration errors differently from transient ones:Custom actions
The editor lets builders define acustom action, and the runtime can dispatch it, but there is no public API on the SDK to register a native handler for a custom action (the registration method exists but is internal). If you need native behavior from a flow, use a custom component that emits an event and wire the action to that event. An unregistered custom action is skipped with a warning, not an error.
Common mistakes
- Not handling the no-flow path.
flowNotFound,targetingNotMet, andfrequencyLimitReachedare normal. Always have something to show (your own UI or nothing), not a broken state. - Treating every error the same. A
placementNotFound(your config is wrong) is not anetworkError(try again later). Branch on the code, or useisClientError/isServerError. - Surfacing raw errors to users.
FlowPilotError.messageis for your logs, not your UI. Map codes to user-facing behavior. - Relying on
setErrorCallback. It does not fire in the current build. Use thrown errors andFlowResult.error. - Using the throwing API on a critical path with no fallback. For onboarding and paywalls, use the never-throwing overload so a network blip cannot strand the user.
Troubleshooting
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 render failure — 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 crash or signal 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.
disableErrorReporting: true in your configuration and nothing is sent.