Skip to content

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.

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 scenarioSDK behavior
Launched Playbook not fetched yet (hosted mode)Placements return visible: false
Entitlement check failsReturns { status: 'denied', allowed: false, reason: 'config_unavailable' }
Config fetch failsFalls back to cached Playbook; with none, entitlement checks deny
Event delivery failsEvents are buffered and retried silently

Hooks expose errors as strings — they never throw:

React
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);
}

Headless controllers surface errors through state:

TypeScript
const ctrl = new PlacementController(sdk, config);
await ctrl.load();
if (ctrl.state.error) {
console.warn('Controller error:', ctrl.state.error);
}

Most SDK methods fail silently and return sensible defaults:

TypeScript
// Denies ({ allowed: false }) when it can't justify a grant
await sdk.can('data_export');
// Silently drops event on delivery failure
await sdk.track('page_viewed');
// Returns null decision on failure
await sdk.getPlacement({ slotId: 'banner' });

When the provider chain is exhausted (all providers failed), slots behave according to providerFailureSlotBehavior:

React
<RevTurbineProvider
options={{
...options,
providerFailureSlotBehavior: 'invisible', // default
}}
>
<YourApp />
</RevTurbineProvider>
ValueBehavior
'invisible'Slots render nothing — your layout stays intact
'placeholder'Slots render fallback placeholder content

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.

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.

CodeMeaning
config_unavailableThe Playbook could not be fetched, or a fetch is still in flight — the SDK has no config to decide against yet.
entitlement_gate_unmetThe candidate’s entitlement-gate trigger did not match the user’s current entitlement state.
entitlement_not_deniedThe Access Gate placement’s entitlement is not denied or limited for this user, so the gate does not fire.
gate_outside_access_gateAn Access Gate placement was requested from a slot that is not an Access Gate slot; gates render only in gate slots.
no_eligible_candidateNo candidate placement survived eligibility filtering for this slot.
no_gate_for_entitlementThe Access Gate slot asks for an entitlement that no gate placement is authored for, so the slot shows its access-denied placeholder.
no_resolver_configuredNo placement resolver is configured, so the SDK has nothing to evaluate against.
placement_not_foundNo candidate output matched the requested placement id or name.
placement_not_registeredThe payload targets a placement handle the app never registered with the SDK.
placement_retiredThe candidate’s rule is hidden — for example, superseded by a conversion — per impression history.
plan_target_mismatchThe user’s plan or billing period doesn’t match the candidate’s plan targeting.
qualifier_trigger_unmetThe candidate’s qualifier trigger condition was not satisfied for the user’s plan or category.
sdk_disabled_provider_failureThe SDK disabled itself after its configured providers failed, so it fails closed rather than risk showing something wrong.
segment_target_mismatchThe user doesn’t belong to any of the candidate’s target segments.
suppressed_by_dismiss_cooldownThe user dismissed this placement and its cooldown window has not elapsed.
suppressed_by_payload_cap_dayThe payload’s per-day impression cap has been reached.
suppressed_by_payload_cap_lifetimeThe payload’s lifetime impression cap has been reached.
suppressed_by_payload_cap_monthThe payload’s per-month impression cap has been reached.
suppressed_by_payload_cap_sessionThe payload’s per-session impression cap has been reached.
suppressed_by_payload_cap_weekThe payload’s per-week impression cap has been reached.
suppressed_by_payload_cooldownThe payload is still inside its configured cooldown window since it was last shown.
suppressed_by_presentation_capEvery eligible candidate is over an overall presentation cap rule from Placement Settings.
suppressed_by_system_cooldownEvery eligible candidate is a discretionary nudge inside the session cooldown since the last one shown.
suppressed_until_remind_windowThe user chose “remind me later” and that reminder window has not elapsed yet.
threshold_trigger_unmetThe candidate’s threshold trigger did not match the user’s current entitlement usage.
trial_trigger_unmetThe candidate’s trial trigger did not match the user’s trial state.
React
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 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.

ReasonMeaning
no_matching_entitlement_ruleNo 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_planA matching feature rule has enabled: false.
usage_limit_reachedThe user is at or over a usage_limit rule’s limit.
credit_balance_exhaustedThe 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:

SuffixenforcementOutcome
(none)hard_blockdenied — hard stop
(none)unsetlimited, not allowed
_block_with_upsellblock_with_upselldenied — render the upsell placement
_degradeddegradelimited but allowed (throttled, not blocked)
_overageallow_overageallowed — 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.

ReasonMeaning
config_unavailableThe launched Playbook could not be fetched (hosted mode). Not a rule decision — an infrastructure failure.
entitlement_not_in_playbookLocal mode with no Playbook and no cached result: nothing describes this entitlement, so there is no basis to grant it.
sdk_disabled_provider_failureThe SDK disabled itself after a provider failure.
ReasonMeaning
granted_by_reverse_trialGranted by an active reverse trial rather than by the user’s plan.
OperationRetry Strategy
Placement resolutionNo auto-retry. Call refresh() to retry manually.
Entitlement checkNo auto-retry. Call recheck() to retry manually.
Event deliveryAuto-buffered and retried on next batch interval.
Config fetchKeeps the last-known-good Playbook; hosted mode re-checks for a new release about once a minute.
React
const { refresh, error } = usePlacement({ placement: { name: 'hero_banner' } });
const { recheck, error: entError } = useEntitlement({ handle: 'data_export' });
// Retry after transient failure
if (error) await refresh();
if (entError) await recheck();

Structure your components so the baseline UI never depends on a placement being present:

React
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.

Enable verbose logging to diagnose issues:

TypeScript
// In browser console
localStorage.setItem('revturbine:debug', 'true');

This logs decision resolution, provider chain evaluation, and error details to the browser console.