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
Section titled “On a slow network, the visitor reads your copy”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 lands in the space it already holds, with one short fade, and that is then what the visitor reads for the page view. A stream that arrives after that is not shown.
Server-rendered pages hold the same way: the server HTML carries the invisible reservation, not your fallback as text. If the page’s JavaScript is late, a CSS-only failsafe reveals your fallback after 5 seconds (at once with scripting off), and that fallback is then final.
If the stream fails while writing a line, what it already wrote stays, even an unfinished line. Only text that is clearly an error is replaced by your fallback, once.
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 |
Before the stream exists: your fallback, reserved invisibly. |
holding |
Your artwork, the text slots held for a moment, their space reserved by your invisible fallback. |
live |
Adapted copy, arriving. |
settled |
Final copy for this page view. |
failed |
Delivered copy stands, unfinished lines included; a blank slot gets your fallback, and only text that is clearly an error is replaced by it. |
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.