When things go wrong
Nothing here throws, and no failure blanks copy a visitor is reading. Copy that already landed stays; anything unfinished falls back to what you wrote.
If our service is down, your copy stays on the page
Section titled “If our service is down, your copy stays on the page”The request fails and state.phase becomes failed. Fields that finished keep
the copy they delivered; a field the stream opened but never finished is
repainted with your whole fallback and demoted to source: "static", so its
AI label goes with it. A field still blank gets your fallback. state.error
carries a readable message you can log. No exception reaches your render tree,
no element collapses, nothing shifts.
There is no retry loop and no reconnection storm: one attempt per block per page view.
On a slow network, the visitor reads your copy and may never see a swap
Section titled “On a slow network, the visitor reads your copy and may never see a swap”On an ordinary client-side mount nothing is on screen yet. The hold arms the moment the block mounts, before the browser ever paints, covering even a wait that precedes the request itself, such as a pinned sibling waiting on the page’s variant draw. The block waits over your artwork rather than flashing copy it is about to replace: 2.5 seconds, restarted once if the stream proves it is alive, so up to 5 seconds. The wait reserves rather than blanks: your fallback is drawn invisibly in its exact space, so nothing collapses while it lasts. A connect that hangs or never answers is the same case: at the end of the budget your fallback types into the space it already holds, and anything that arrives later lands as the one permitted change. Every entrance is a typed entrance, fallback included. On a client-side mount that is the only way a visitor reads your fallback first and then watches it change.
If the block hydrated over server-rendered HTML, your fallback is already being read: it is exempt from the hold, and it is never typed over. Adapted text buffers off-canvas and lands in one piece, at most one composed change for the whole page view.
If the campaign is paused or out of budget, real copy still renders
Section titled “If the campaign is paused or out of budget, real copy still renders”The server can serve a fixed, pre-decided version instead of generating one.
That is not an error: the words are real, they were simply not adapted for this
visitor. state.degraded names the reason and state.contentSource says
static, so nothing claims adapted provenance. Correctly, because nothing was
generated.
Hero puts it on the element as data-degraded, if you want to annotate it.
Every state a visitor can be in
Section titled “Every state a visitor can be in”state.phase | On screen |
|---|---|
idle | Your fallback. Nothing requested yet. |
holding | Your artwork, replaceable copy briefly withheld, its space reserved by your invisible fallback. |
live | Adapted copy, arriving. |
settled | Final copy for this page view. |
failed | Delivered copy stands; anything unfinished or blank is repainted with your fallback. |
Test the outage in one line
Section titled “Test the outage in one line”Point the provider at a host that cannot answer:
<SparkletreeProvider apiBase="https://127.0.0.1:9" organizationId="org_123">Expect: your fallback, readable; state.rewrites === 0; no adapted
provenance; no layout shift. Verify this before you verify the happy path. It is the
behaviour that runs during an incident.
For previews, Storybook and unit tests, skip the network instead of faking a broken host:
useIsland({ campaignId, island: "hero", fallback, enabled: false });The one failure you have to fix yourself: a missing ref
Section titled “The one failure you have to fix yourself: a missing ref”useIsland returns a ref that must be attached to the element which has to be
visible for the view to count. Forget it and the campaign renders perfectly
while reporting zero impressions. Because the SDK tells the server it
owns the count, nothing else counts it either.
const { state, ref } = useIsland({ … });return <section ref={ref}>…</section>;Hero and Cta attach it for you. There is no warning when a custom component
does not.
Failures the SDK absorbs without telling you
Section titled “Failures the SDK absorbs without telling you”- A blocked or throwing
localStorage. The visitor’s “standard version” preference cannot be stored; copy keeps counting as adapted (decideChromestays honest), which is the safe default. Pass your ownstorageif a consent manager gates it. - A missing
IntersectionObserver. The impression fires immediately instead of waiting for visibility. Over-counting is visible in your numbers; under-counting would be a silent bias in figures you are billed on. - Missing context signals. A hardened browser or in-app webview may withhold language, device or timezone. The creative adapts less well; nothing breaks.
- An API version your deployment does not implement. Your first paint stands and no retry happens.
- A self-hosted
apiBasethat arrived through apublishableKey. The request that writes new fragment copy is withheld, so fragments serve your fallbacks and never warm up. This is the one entry here that does not resolve itself with time. PassapiBaseandorganizationIdto the provider explicitly. A dev build warns once in the console; see signed minting.
What a failure leaves untouched
Section titled “What a failure leaves untouched”Painted text stays painted, your artwork stays, your DOM keeps its order, your scripts keep running, and no spinner is left spinning. A sentence is never left half-typed either. An abandoned field is repainted whole from your fallback, so the visitor always reads something finished.