{"record":{"id":"dde66f59697abcd4","repo":"phaserjs/phaser","slug":"invalid-operation","errorCode":"INVALID_OPERATION","errorMessage":"Can not perform this operation before game start.","messagePattern":"Can not perform this operation before game start\\.","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"changelog/v3/3.20/CHANGELOG-v3.20.md","lineNumber":59,"sourceCode":"* `Device.Video.oggVideo` has been renamed to `Device.Video.ogg` to keep it in-line with the Audio Device names.\n* `Device.Video.vp9Video` has been renamed to `Device.Video.vp9` to keep it in-line with the Audio Device names.\n* `Device.Video.webmVideo` has been renamed to `Device.Video.webm` to keep it in-line with the Audio Device names.\n\n### Spine Plugin\n\n* The Spine runtimes have been updated to 3.8. Please note that Spine runtimes are _not_ backwards compatible. Animations exported with Spine 3.7 (or earlier) will need re-exporting with 3.8 in order to work with the new runtimes.\n* Fixed a bug with the binding of the Spine Plugin causing the GameObjectFactory to remain bound to the first instance of the plugin, causing Scene changes to result in blank Spine Game Objects. Fix #4716 (thanks @olilanz)\n* Fixed a bug with the caching of the Spine Texture Atlases, causing shader errors when returning from one Scene to another with a cached Texture Atlas.\n* The WebGL Scene Renderer is now only disposed if the Scene is destroyed, not just shut-down.\n* The Spine Game Object will no longer set the default skin name to be 'default', it will leave the name empty. Fix #4764 (thanks @Jonchun @badlogic)\n* Thanks to a fix inside the Container WebGLRenderer, a bug was crushed which involved multiple Containers in a Scene, with Spine objects, from causing run-time errors. Fix #4710 (thanks @nalgorry)\n* Using `Loader.setPath` to define the Spine assets locations could error if trying to load multiple files from different folders. It will now retain the path state at the time of invocation, rather than during the load.\n* When loading Spine files that used the same internal image file names, only the first file would successfully load. Now, all files load correctly.\n\n### Facebook Instant Games Plugin\n\n* Calling `showAd` or `showVideoAd` will now check to see if the ad has already been displayed, and skip it when iterating the ads array, allowing you to display an ad with the same Placement ID without preloading it again. Fix #4728 (thanks @NokFrt)\n* Calling `gameStarted` in a game that doesn't load any assets would cause the error `{code: \"INVALID_OPERATION\", message: \"Can not perform this operation before game start.\"}`. The plugin will now has a new internal method `gameStartedHandler` and will redirect the flow accordingly based on asset loading. Fix #4550 (thanks @bchee)\n* The documentation for the `chooseContext` method has been fixed. Fix #4425 (thanks @krzysztof-grzybek)\n* `Leaderboard.getConnectedScores` incorrectly specified two parameters, neither of which were used. Fix #4702 (thanks @NokFrt)\n* `Leaderboard` extends Event Emitter, which was missing in the TypeScript defs. Fix #4703 (thanks @NokFrt)\n\n### Arcade Physics Updates\n\n@BenjaminDRichards and the GameFroot team contributed the following updates to Arcade Physics, which fixes 3 issues encountered when the framerate drops below 60 (technically, any time when multiple physics steps run per frame, so if physics FPS is above 60 this will also occur.)\n\nIssue 1: Friction starts to flip out. Objects on moving platforms get pushed ahead of the platform and \"catch\" on the leading edge.\nIssue 2: Physics objects start to dip into the floor. In the \"Before\" demo, the camera is locked to the player, so this appears as the entire world starting to shake up and down.\nIssue 3: When objects dip into the floor, their \"rest velocity\" is non-zero. This can affect debug and other logic.\n\n* `Body.prevFrame` is a new vector allowing a Body to distinguish between frame-length changes and step-length changes. Several steps may run for every frame, particularly when fps is low.\n* `Body._reset` flag was removed and replaced it with a check of `Body.moves`. The flag only turned on when `moves` was true, and never turned off.\n* Added a reset of `prev` in Arcade.Body#step. This fixes the friction issue.\n* Stopped the `Body.postUpdate` method from setting `_dx`, `_dy`, and `prev`. They remain in the state they were at the end of the last physics step. This will affect the delta methods, which are documented to provide step-based data (not frame-based data); they now do so. However, because several steps may run per frame, you can't interrogate every step unless you're running functions based on physics events like collisions. You'll just see the latest step. This should partially balance out the extra load of resetting prev.\n* Added a zero-out of stepsLastFrame in Arcade.World#postUpdate, which would otherwise never zero out and keep running at least one pass per frame. This should improve performance when frames can be skipped.\n* Removed `blocked` checks from `TileCheckX` and `TileCheckY`. Originally, this prevented multiple checks when an object had come to rest on a floor. However, when multiple steps run per frame, the object will accelerate again, the floor won't stop it on steps 2+, and it will end the frame a short distance into the floor. Removing the blocked checks will fix the floor dip issue and the rest velocity issue. Although this opens up multiple checks, this is probably very rare: how many times does an object hit two different floors in a single frame?","sourceCodeStart":41,"sourceCodeEnd":77,"githubUrl":"https://github.com/phaserjs/phaser/blob/41be1e462bc600064e498cba370bfa8c5c055a22/changelog/v3/3.20/CHANGELOG-v3.20.md#L41-L77","documentation":"This is NOT a Phaser-thrown error; it originates from the Facebook Instant Games SDK (FBInstant), code INVALID_OPERATION, returned when an API is called before the game session has started. Phaser's Facebook Instant Games Plugin historically forwarded `gameStarted` to FBInstant.startGameAsync unconditionally; if the game loaded no assets, the plugin called startGameAsync before the SDK was ready, surfacing this SDK error. Phaser 3.20 fixed it by adding an internal `gameStartedHandler` that branches on whether assets are loading and only calls startGameAsync once loading completes.","triggerScenarios":"In a Phaser 3.19-or-earlier project using the Facebook Instant Games plugin, calling `this.facebook.gameStarted()` (or having the plugin call it automatically) in a game that loads zero assets. The plugin invoked FBInstant.startGameAsync before the FBInstant environment was in the right state, and the SDK rejected with `{code: 'INVALID_OPERATION', message: 'Can not perform this operation before game start.'}`. After 3.20 the same code path is safe.","commonSituations":"Minimal Phaser FB games with no preload step. Games that call gameStarted in create() before any Loader.addPack. Upgrading the FBInstant SDK to a version with stricter startGameAsync ordering without updating Phaser. Testing outside the FB environment (the SDK throws when not in an iframe wrapper).","solutions":["Upgrade Phaser to 3.20.0 or later, which contains the gameStartedHandler fix.","Ensure at least one asset is loaded via the Phaser loader before calling gameStarted, so the plugin routes through the load-complete callback.","If you must call FBInstant directly, await FBInstant.initializeAsync() before FBInstant.startGameAsync(), then start the Phaser scene.","If the error still appears post-upgrade, confirm you are actually running inside the Facebook Instant Games wrapper (window.FBInstant defined and not in a test harness)."],"exampleFix":"// before (Phaser < 3.20, no assets loaded)\nthis.facebook.gameStarted();\n\n// after (Phaser >= 3.20, or explicit guard)\n// Option A: load something first\nthis.load.image('logo', 'logo.png');\nthis.load.on('complete', () => this.facebook.gameStarted());\n// Option B: call the FB SDK in order yourself\nFBInstant.initializeAsync().then(() => FBInstant.startGameAsync()).then(() => this.scene.start('Menu'));","handlingStrategy":"validation","validationCode":"function safeGameStarted(plugin) {\n  if (!window.FBInstant) return; // not in FB environment\n  if (plugin.game && plugin.game.load && plugin.game.load.totalToLoad > 0) {\n    plugin.game.load.once('complete', () => plugin.gameStarted());\n  } else {\n    // upgrade Phaser to >=3.20 where the plugin handles this internally\n    plugin.gameStarted();\n  }\n}","typeGuard":"function isInstantGamesReady() {\n  return typeof window !== 'undefined' && !!window.FBInstant && typeof window.FBInstant.startGameAsync === 'function';\n}","tryCatchPattern":"if (window.FBInstant) {\n  FBInstant.initializeAsync()\n    .then(() => FBInstant.startGameAsync())\n    .then(() => this.facebook.gameStarted())\n    .catch((err) => {\n      if (err && err.code === 'INVALID_OPERATION') {\n        // SDK not ready; retry after next tick or asset load\n      } else { throw err; }\n    });\n}","preventionTips":["Upgrade Phaser to 3.20.0 or later; the plugin's gameStartedHandler fixes the ordering.","Always load at least one asset so the plugin routes gameStarted through load-complete.","Call FBInstant.initializeAsync() then startGameAsync() in sequence before starting the Phaser scene.","Detect the FB environment (window.FBInstant) before invoking any plugin method; the SDK throws when not in the wrapper."],"tags":["facebook-instant-games","sdk","lifecycle","version-upgrade","platform"],"backgroundTag":null,"analyzedSha":"41be1e462bc600064e498cba370bfa8c5c055a22","analyzedAt":"2026-08-13T04:23:39.729Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}