Skip to main content
Showing a flow depends on the network, your configuration, and a valid flow schema, so it can fail. FlowPilot is built so that a failure never crashes your app or leaves a blank screen, but you still decide what the user sees when there is no flow to show. This page covers the error type, the error codes, and the resilient presentation path you should use for anything user-facing.

The error type

Every failure the SDK surfaces is a FlowPilotError:
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:
  • flowNotFound means 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.
  • placementNotFound means the placement id does not exist. That is a configuration mistake, so check the id against the dashboard.
  • timeout means the resolve did not finish within resolveTimeout. The SDK falls back to cache or bundled flows first; you only see a thrown timeout if nothing was cached.

Where errors actually surface

There are three reliable ways to observe errors today:
  1. Thrown from the throwing APIs. presentPlacement(_:from:options:) and createSession(...) are throws. Wrap them in do/catch and inspect the FlowPilotError.
  2. As a .error outcome on FlowResult. When a flow ends in error, result.outcome == .error and result.error holds the FlowPilotError. See Results and outcomes.
  3. Never thrown, by design. The fallback overloads below never throw. They return a FlowResult (with .error if they had to fall back to your own UI) or nil.
setErrorCallback(_:) exists and is public, but in the current SDK build the stored closure is never invoked anywhere in the SDK (it is assigned and then not read). Do not rely on it to receive errors yet. Handle errors through the thrown FlowPilotError and FlowResult.error instead.The same applies to the FlowPilotDelegate protocol: it is declared public but has no setter and is never called. Use the closure-based APIs and the resilient overloads, not the delegate.

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 by resolveTimeout, then stale cache, then a bundled default flow) before giving up:
With these, a network failure cannot block onboarding: the user gets a cached flow, a bundled default, or your own native screen. See Offline and bundled flows, Caching, and Prefetching.

Example

If you use the throwing API, catch the error and branch on its code. Treat configuration errors differently from transient ones:
The cleaner option is to let the SDK do this for you with the never-throwing overload:

Custom actions

The editor lets builders define a custom 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, and frequencyLimitReached are 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 a networkError (try again later). Branch on the code, or use isClientError / isServerError.
  • Surfacing raw errors to users. FlowPilotError.message is 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 and FlowResult.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.
What’s sent: the error code and message, the SDK version, the device / 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.