Troubleshooting
Symptom → likely cause → how to verify → fix. The page you reach for when something silently doesn't work.
When the SDK doesn’t behave the way you expected, this page is the fastest
path from a symptom to a root cause. The errors page catalogues
what each GameeError code means; this page is the other half — the
symptoms that don’t raise a typed error and the ones that look like one
problem but are really another.
Each section has the same shape: Symptom → Likely cause → How to verify → Fix. Scan the symptom headings; jump in when one matches.
init() never resolves#
Symptom. Your boot code awaits gamee.init(...) and the promise hangs.
Eventually (after 30 s by default) it rejects with BRIDGE_TIMEOUT — or it
never resolves at all because you forgot the await.
Likely causes, in order of frequency:
- You’re running the game outside a GAMEE host. Opening the HTML file
directly, previewing in a search engine, or running
astro devstandalone means there’s no platform on the other side of the bridge. - On web: the platform’s origin isn’t in
allowedOrigins. InboundpostMessagemessages from origins outside the list are dropped without a warning by default. To the SDK, the platform looks silent. - On iOS/Android: the native handler isn’t wired up. The host app needs
webkit.messageHandlers.callbackHandler(iOS) orwindow._toDevice(Android) installed before the game loads. A misconfigured host app looks identical to “no host at all” from the SDK’s point of view.
How to verify.
- Wrap your boot in
hasPlatformContext()before callinginit(). If it returnsfalse, you’re not inside a real host — render a fallback instead of hanging. - Open the emulator, point it at your game URL, and watch the
Bridge traffic panel. If you see your
initrequest leave the game but never get a response, the host side isn’t replying. - On web, check
window.parent.postMessageis being received: temporarily setallowedOrigins: '*'increateSdk()and see ifinitresolves. If it does, your real origin list is too strict — add the platform’s origin.
Fix. Always guard boot with hasPlatformContext(). In production,
configure allowedOrigins explicitly so messages from the real platform
origin are accepted (config). On native, talk to
the host-app team about handler wiring.
Saved state isn’t coming back#
Symptom. You call gamee.gameSaveState(...), the next session starts,
and initResponse.saveState is undefined (or stale).
Likely causes.
- You didn’t declare the
saveStatecapability ininit(). The SDK throwsCAPABILITY_MISSINGsynchronously the first time you callgameSaveState— but if you swallowed the error, you’d never notice. - You called
gameSaveStatebeforeinit()resolved. Signals validate immediately but rely on the SDK being in the post-init state to actually ship. A pre-init call rejects synchronously withINVALID_STATE. - You passed un-stringifiable data (a
Function, a circular ref). The SDK doesJSON.stringifyon object values and that throws. - The platform didn’t return the previous save in this session. This is a platform decision, not an SDK bug — for example, the user reset their save from the platform UI.
How to verify.
console.log(gamee.initResponse?.saveState)right afterinit()resolves. If it’sundefined, the platform either has no save or chose not to send it.- Open the emulator and inspect the wire: each
gameSaveStatecall should appear as{ method: 'saveState', data: { state: '<string>' } }. - Wrap
gameSaveStateintry/catch(or.catch) and logerr.code—CAPABILITY_MISSINGis the most common surprise.
Fix. Declare 'saveState' in the capabilities array on init(). Pass
plain JSON-serializable values (objects of primitives + arrays + nested
objects).
Events I subscribed to never fire#
Symptom. gamee.on('start', ...) is registered but the handler is never
invoked. Same for pause, resume, etc.
Likely causes.
- You subscribed after calling
gameReady(). The platform may dispatch immediately ongameReady(). See Events → subscribe beforegameReady()— this is the canonical rule and the most common cause. - The event is capability-gated and you didn’t declare the capability.
useExtraLiferequiresplatformExtraLifeininit(); without it the platform won’t dispatch the event at all. init()never resolved (see the first section). No events fire before the handshake completes.
How to verify.
- Move every
gamee.on(...)call abovegamee.gameReady(). If the events start firing, ordering was the problem. - For
useExtraLife, confirm'platformExtraLife'is in yourinit()capabilities array. - Check the emulator’s bridge traffic for incoming events — if the platform is sending them but your handler isn’t running, you’ve got a subscription order or capability bug.
Fix. Subscribe before gameReady(). Declare every capability you rely
on at init.
Validation errors on logEvent / updateScore / updateMissionProgress#
Symptom. A signal call rejects with GameeError({ code: 'VALIDATION' }).
Because signals are fire-and-forget, you may only see the rejection as an
“unhandled promise rejection” in the console.
Likely causes. Each has a hard limit enforced locally:
| Method | Constraint |
|---|---|
logEvent | name ≤ 24 chars, non-empty; value ≤ 160 chars (string). |
updateScore | score finite; playTime ≥ 0 and finite; checksum non-empty if set. |
updateMissionProgress | progress in [0, 100], finite. |
purchaseItemWithGems | gemsCost ≥ 0; itemName non-empty. |
init | Every capability string must be one of the known six. |
How to verify. Wrap the call in try/catch (or .catch) and log
err.method plus err.message. The message names the failing field.
Fix. Clamp / sanitize at the call site. Don’t ship a feature that emits
logEvent names longer than 24 chars and rely on the platform to truncate
— it won’t.
Rewarded ad never plays#
Symptom. gamee.showRewardedVideo() rejects, resolves with
{ videoPlayed: false } instantly, or hangs.
Likely causes.
- You didn’t call
loadRewardedVideo()first (or its result was{ videoLoaded: false }).showRewardedVideoshows what’s already loaded; it doesn’t load on demand. - The
rewardedAdscapability isn’t declared. Both load and show throwCAPABILITY_MISSINGsynchronously. - You’re not in a real GAMEE host. The emulator stubs the ad UI, but in a standalone browser there’s no ad provider to talk to.
noAds: trueininitResponse. The user has paid for an ad-free experience. Rewarded ads remain opt-in butloadRewardedVideomay legitimately return{ videoLoaded: false }.
How to verify. Log the loadRewardedVideo result before offering the
“watch ad” UI. Check gamee.initResponse?.noAds.
Fix. Pre-load before offering. Hide the “watch ad” button when
videoLoaded is false. Treat a false result as the platform’s final
answer, not an error.
”Works on web, broken on iOS/Android” (or vice versa)#
Symptom. The same build behaves differently across platforms — events
that fire on web don’t on mobile, or init resolves on iOS but hangs on
Android.
Likely causes.
- Platform-specific code that branches on user agent. Don’t sniff the
UA; use
sdk.platformafterinit()orinitResponse.platform. - iOS/Android don’t run in an iframe — they load as the top-level
document inside
WKWebView/WebView. Code that assumeswindow.parent !== windowis wrong on mobile. - The native host app uses an older bridge implementation that doesn’t
know about a newer SDK method. The SDK will see
BRIDGE_TIMEOUTbecause the host never replies.
How to verify. Log initResponse.platform and initResponse.environment
on every device you test. Reproduce the failing wire traffic in the
emulator (it can simulate each platform).
Fix. Treat the bridge as authoritative and never branch on UA. When you
genuinely need per-platform behavior (audio unlock semantics differ, for
instance), branch on initResponse.platform. For host-side mismatches,
coordinate with the host-app team — there is no SDK-side workaround.
INVALID_STATE from a method call#
Symptom. A method rejects with GameeError({ code: 'INVALID_STATE' }).
Likely causes.
init()was called twice on the same SDK instance. See Methods →initis one-shot. The fix iscreateSdk()in tests, not a secondinit().- A method ran after
dispose(). All pending Promises reject withINVALID_STATEoncedispose()is called. - A signal ran before
init()resolved. Pre-init signals reject immediately because the SDK has no platform context yet.
How to verify. Log the call order in the failing path. The order is
always init → on(...) → gameReady → everything else. A signal that
fires from a top-level module body (before await init) trips this.
Fix. Keep boot logic inside the async function that awaits init.
For repeated init in tests use createSdk() per test, dispose in
afterEach.
My signal call rejected and I didn’t notice#
Symptom. Behavior silently wrong; “unhandled promise rejection” in the console.
Likely cause. Signal methods return Promise<void> that resolves
immediately on success but rejects on validation / capability failures
before the call ever hits the wire. If you neither await nor .catch()
them, those rejections surface as unhandled.
Fix. For signals you don’t want to await, attach a .catch:
gamee.updateScore(score).catch((err) => {
if (err instanceof GameeError) console.warn(err.code, err.method);
});
Or set the top-level safety net once at
boot so every unhandled GameeError lands in analytics.