Error Handling
import { Aside } from ‘@astrojs/starlight/components’;
The SDK never throws into your app and never blocks your render. But placements and entitlement checks degrade in opposite directions, on purpose: a placement that can’t resolve renders nothing, while an entitlement check that can’t resolve denies.
Two degradation modes
Section titled “Two degradation modes”A placement that can’t resolve renders nothing (or your configured fallback), so it can never take your product down.
Entitlement checks are fail-closed. If a check can’t produce an affirmative grant, it returns { status: 'denied', allowed: false } rather than granting access. The Playbook is cached and persisted locally, so a configured runtime evaluates real allow/deny answers with no network round-trip; the failure fallback only fires when the SDK has no basis to answer at all — no config, no cache, nothing reachable — which is exactly where denying is the safe, non-leaking default. The reason code is preserved so you can still tell an infrastructure failure apart from a real denial.
| API failure scenario | SDK behavior |
|---|---|
| Launched Playbook not fetched yet (hosted mode) | Placements return visible: false |
| Entitlement check fails | Returns { status: 'denied', allowed: false, reason: 'config_unavailable' } |
| Config fetch fails | Falls back to cached Playbook; with none, entitlement checks deny |
| Event delivery fails | Events are buffered and retried silently |
Error Surface
Section titled “Error Surface”Hook Level
Section titled “Hook Level”Hooks expose errors as strings — they never throw:
const { error, isLoading } = usePlacement({ placement: { name: 'hero_banner' } });const { error: entError } = useEntitlement({ handle: 'data_export' });
if (error) { // Non-critical — log and continue console.warn('Placement error:', error);}Controller Level
Section titled “Controller Level”Headless controllers surface errors through state:
const ctrl = new PlacementController(sdk, config);await ctrl.load();
if (ctrl.state.error) { console.warn('Controller error:', ctrl.state.error);}SDK Level
Section titled “SDK Level”Most SDK methods fail silently and return sensible defaults:
// Denies ({ allowed: false }) when it can't justify a grantawait sdk.can('data_export');
// Silently drops event on delivery failureawait sdk.track('page_viewed');
// Returns null decision on failureawait sdk.getPlacement({ slotId: 'banner' });Provider Failure Behavior
Section titled “Provider Failure Behavior”When the provider chain is exhausted (all providers failed), slots behave according to providerFailureSlotBehavior:
<RevTurbineProvider options={{ ...options, providerFailureSlotBehavior: 'invisible', // default }}> <YourApp /></RevTurbineProvider>| Value | Behavior |
|---|---|
'invisible' | Slots render nothing — your layout stays intact |
'placeholder' | Slots render fallback placeholder content |
Recommendation
Section titled “Recommendation”Use 'invisible' (default) for production. Your app should work fine without any placement rendering.
Use 'placeholder' during development to visually verify that slots are wired correctly even when a provider fails.
Reason Codes
Section titled “Reason Codes”Placement decisions include reasonCodes that explain why a placement was hidden or shown. This is the complete set the SDK emits, generated from the public contract in tests/reason-contract.json.
An interaction-flush failure and a client-side cap-enforcement fallback are each documented in their own place on the Error Codes reference page instead of the table below, since neither is part of that contract.
| Code | Meaning |
|---|---|
config_unavailable | The Playbook could not be fetched, or a fetch is still in flight — the SDK has no config to decide against yet. |
entitlement_gate_unmet | The candidate’s entitlement-gate trigger did not match the user’s current entitlement state. |
entitlement_not_denied | The Access Gate placement’s entitlement is not denied or limited for this user, so the gate does not fire. |
gate_outside_access_gate | An Access Gate placement was requested from a slot that is not an Access Gate slot; gates render only in gate slots. |
no_eligible_candidate | No candidate placement survived eligibility filtering for this slot. |
no_gate_for_entitlement | The Access Gate slot asks for an entitlement that no gate placement is authored for, so the slot shows its access-denied placeholder. |
no_resolver_configured | No placement resolver is configured, so the SDK has nothing to evaluate against. |
placement_not_found | No candidate output matched the requested placement id or name. |
placement_not_registered | The payload targets a placement handle the app never registered with the SDK. |
placement_retired | The candidate’s rule is hidden — for example, superseded by a conversion — per impression history. |
plan_target_mismatch | The user’s plan or billing period doesn’t match the candidate’s plan targeting. |
qualifier_trigger_unmet | The candidate’s qualifier trigger condition was not satisfied for the user’s plan or category. |
sdk_disabled_provider_failure | The SDK disabled itself after its configured providers failed, so it fails closed rather than risk showing something wrong. |
segment_target_mismatch | The user doesn’t belong to any of the candidate’s target segments. |
suppressed_by_dismiss_cooldown | The user dismissed this placement and its cooldown window has not elapsed. |
suppressed_by_payload_cap_day | The payload’s per-day impression cap has been reached. |
suppressed_by_payload_cap_lifetime | The payload’s lifetime impression cap has been reached. |
suppressed_by_payload_cap_month | The payload’s per-month impression cap has been reached. |
suppressed_by_payload_cap_session | The payload’s per-session impression cap has been reached. |
suppressed_by_payload_cap_week | The payload’s per-week impression cap has been reached. |
suppressed_by_payload_cooldown | The payload is still inside its configured cooldown window since it was last shown. |
suppressed_by_presentation_cap | Every eligible candidate is over an overall presentation cap rule from Placement Settings. |
suppressed_by_system_cooldown | Every eligible candidate is a discretionary nudge inside the session cooldown since the last one shown. |
suppressed_until_remind_window | The user chose “remind me later” and that reminder window has not elapsed yet. |
threshold_trigger_unmet | The candidate’s threshold trigger did not match the user’s current entitlement usage. |
trial_trigger_unmet | The candidate’s trial trigger did not match the user’s trial state. |
Inspecting Reason Codes
Section titled “Inspecting Reason Codes”const { decision } = usePlacement({ placement: { name: 'hero_banner' } });
if (decision?.reasonCodes?.includes('suppressed_by_payload_cap_day')) { // User has hit this payload's daily impression cap}Entitlement Reason Codes
Section titled “Entitlement Reason Codes”Entitlement checks are fail-closed. When the SDK cannot produce an
affirmative grant — the Playbook never arrived, no rule grants the entitlement
to the user’s plan, the SDK was disabled — it returns
{ status: 'denied', allowed: false } with a reason naming the cause. It
never invents a grant. Treat allowed as the answer and reason as the
explanation; the codes below are the complete set the SDK emits.
Denied because a rule said so
Section titled “Denied because a rule said so”| Reason | Meaning |
|---|---|
no_matching_entitlement_rule | No rule grants this entitlement to the user’s plan. Plan targeting is explicit — an entitlement with no enabling rule for that plan is not granted. |
feature_not_enabled_for_plan | A matching feature rule has enabled: false. |
usage_limit_reached | The user is at or over a usage_limit rule’s limit. |
credit_balance_exhausted | The user is at or over a credits rule’s allowance. |
usage_limit_reached and credit_balance_exhausted carry a suffix reflecting
the rule’s enforcement mode:
| Suffix | enforcement | Outcome |
|---|---|---|
| (none) | hard_block | denied — hard stop |
| (none) | unset | limited, not allowed |
_block_with_upsell | block_with_upsell | denied — render the upsell placement |
_degraded | degrade | limited but allowed (throttled, not blocked) |
_overage | allow_overage | allowed — metered overage |
Two modes produce no suffix and they are not equivalent: hard_block
denies, while leaving enforcement unset returns limited and does not allow.
Branch on allowed, never on the presence of a suffix.
Denied because the SDK could not decide
Section titled “Denied because the SDK could not decide”| Reason | Meaning |
|---|---|
config_unavailable | The launched Playbook could not be fetched (hosted mode). Not a rule decision — an infrastructure failure. |
entitlement_not_in_playbook | Local mode with no Playbook and no cached result: nothing describes this entitlement, so there is no basis to grant it. |
sdk_disabled_provider_failure | The SDK disabled itself after a provider failure. |
Allowed with a reason
Section titled “Allowed with a reason”| Reason | Meaning |
|---|---|
granted_by_reverse_trial | Granted by an active reverse trial rather than by the user’s plan. |
Retry Behavior
Section titled “Retry Behavior”| Operation | Retry Strategy |
|---|---|
| Placement resolution | No auto-retry. Call refresh() to retry manually. |
| Entitlement check | No auto-retry. Call recheck() to retry manually. |
| Event delivery | Auto-buffered and retried on next batch interval. |
| Config fetch | Keeps the last-known-good Playbook; hosted mode re-checks for a new release about once a minute. |
Manual Retry
Section titled “Manual Retry”const { refresh, error } = usePlacement({ placement: { name: 'hero_banner' } });const { recheck, error: entError } = useEntitlement({ handle: 'data_export' });
// Retry after transient failureif (error) await refresh();if (entError) await recheck();Design for the empty state
Section titled “Design for the empty state”Structure your components so the baseline UI never depends on a placement being present:
function Dashboard() { return ( <div> {/* Baseline UX — always works */} <DashboardContent />
{/* Placement — renders nothing when no placement matches */} <Slot id="dashboard_banner" /> </div> );}When no placement matches the current user, <Slot> renders nothing and the baseline dashboard is unchanged. Targeting, frequency caps and cooldowns can all produce that outcome, so the layout must hold up without it.
Debugging Errors
Section titled “Debugging Errors”Enable verbose logging to diagnose issues:
// In browser consolelocalStorage.setItem('revturbine:debug', 'true');This logs decision resolution, provider chain evaluation, and error details to the browser console.
Next Steps
Section titled “Next Steps”- Configuration Reference — error-related configuration options
- Error Codes Reference — enumerated error codes