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.

Placements are additive. If RevTurbine is paused, misconfigured, or unreachable, a slot renders nothing (or your configured fallback) — 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 outage apart from a real denial.

API failure scenarioSDK behavior
API unreachablePlacements 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:

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:

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:

// Returns false on API failure
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:

<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. Placements are additive — your app should work fine without them.

Use 'placeholder' during development to visually verify that slots are wired correctly even when the provider is down.

Placement decisions include reasonCodes that explain why a placement was hidden or shown:

CodeMeaning
cap_exceededImpression cap reached
suppressedUser recently dismissed/snoozed
plan_mismatchUser’s plan doesn’t match targeting
segment_mismatchUser doesn’t match targeting segment
config_unavailablePlaybook not available
api_errorAPI returned non-200
network_errorNetwork/timeout failure
fallback_contentUsing fallback placeholder
const { decision } = usePlacement({ placement: { name: 'hero_banner' } });
if (decision?.reasonCodes?.includes('cap_exceeded')) {
// User has seen this placement too many times
}

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 (Server 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 fetchFalls back to cached config. Retried on next SDK initialization.
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 SDK enhancement is purely additive:

function Dashboard() {
return (
<div>
{/* Baseline UX — always works */}
<DashboardContent />
{/* SDK enhancement — fails gracefully to nothing */}
<Slot id="dashboard_banner" />
</div>
);
}

If the SDK is down, <Slot> renders nothing and the baseline dashboard continues working.

Enable verbose logging to diagnose issues:

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

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