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 allowed 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 reason_codes that explain why a placement was hidden or shown:

CodeMeaning
cap_limit_exceededImpression cap reached
suppressedUser recently dismissed/snoozed
plan_mismatchUser’s plan doesn’t match targeting
segment_mismatchUser doesn’t match targeting segment
config_not_loadedPlaybook not yet available
api_errorAPI returned non-200
network_errorNetwork/timeout failure
fallback_contentUsing fallback placeholder
const { decision } = usePlacement({ placement: { name: 'hero_banner' } });
if (decision?.reason_codes?.includes('cap_limit_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 defaults to allowed. 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.