Quick answer: Save/load failures in Construct 3 are usually caused by not waiting for the "On load complete" trigger, async local storage race conditions, UID invalidation after load, or missing savegame versioning. Always use trigger-based flow control for save/load operations, avoid hardcoded UIDs, and include a version field in your save data.

Here is how to fix Construct 3 save and load systems that are not working or are losing data. You call the save action, then the load action, and the game state that comes back is wrong — objects are missing, variables have default values, or the load silently fails. Save/load is one of the most error-prone features in Construct 3 because it involves asynchronous operations and serialization edge cases that the event sheet model does not make obvious.

The Symptom

You use the built-in System → Save or System → Load actions (or a custom system using LocalStorage and JSON). After loading, one or more of these things happen:

Instance variables are reset to their default values instead of the saved values. Objects that should exist are missing. Global variables have the wrong values. The layout looks correct but game logic breaks because internal references are stale. Or the load action appears to do nothing at all — the game continues as if load was never called.

If you are using LocalStorage for a custom save system, you might see the value come back as empty or as the string "undefined" instead of your JSON data. Or your JSON.parse call fails with a syntax error.

What Causes This

There are six common causes:

1. Not waiting for "On load complete." The built-in save/load system is asynchronous. When you call System → Load, the load does not happen instantly on that event line. It happens later, and the engine fires the "On load complete" trigger when it is done. If you try to read restored values in the same event or the next sequential event after Load, you are reading the old state.

2. Local storage async race conditions. LocalStorage operations (Get item, Set item) are asynchronous. Calling "Get item" and then immediately reading LocalStorage.ItemValue on the next event gives you stale data. You must use the "On item get" trigger to know when the data is actually available.

3. UID references becoming invalid. The built-in save system recreates all instances on load. The new instances can have different UIDs from the originals. If your game logic stores UIDs to reference specific objects (like "this projectile targets enemy with UID 47"), those references break after load.

4. JSON parse errors in custom save systems. When building a manual save system with JSON, it is easy to produce malformed JSON. Common mistakes include trailing commas, unquoted keys, or trying to serialize objects that contain circular references. A single syntax error in the JSON string causes the entire parse to fail.

5. Global variables not included in custom saves. The built-in save system captures global variables automatically. But if you are building a custom JSON-based system, you must explicitly include every global variable you want to persist. Forgetting even one means it reverts to its initial value on load.

6. No savegame versioning. When you update your game and add new variables, objects, or layouts, old save files do not include that new data. Without a version check, loading an old save into a new version of the game can silently fail or produce corrupted state.

The Fix

Step 1: Always use "On load complete" for the built-in system.

// WRONG: Reading values immediately after Load
Event
  Condition: Keyboard → On "F5" pressed
  Action: System → Save to slot "save1"

Event
  Condition: Keyboard → On "F9" pressed
  Action: System → Load from slot "save1"
  Action: TextDisplay → Set text to "Health: " & Player.Health
  // BUG: Player.Health is still the old value here!

// CORRECT: Wait for the trigger
Event
  Condition: Keyboard → On "F9" pressed
  Action: System → Load from slot "save1"

Event
  Condition: System → On load complete
  Action: TextDisplay → Set text to "Health: " & Player.Health
  Action: Browser → Log "Load finished, health = " & Player.Health
  // NOW Player.Health has the restored value

Step 2: Handle LocalStorage async correctly.

// WRONG: Reading immediately after Get
Event
  Condition: Button → On "Load" clicked
  Action: LocalStorage → Get item "savegame"
  Action: JSON → Parse LocalStorage.ItemValue
  // BUG: ItemValue is empty or stale here!

// CORRECT: Use the On item get trigger
Event
  Condition: Button → On "Load" clicked
  Action: LocalStorage → Get item "savegame"

Event
  Condition: LocalStorage → On item "savegame" get
  Action: JSON → Parse LocalStorage.ItemValue
  Action: Set Player.Health to JSON.Get("health")
  Action: Set Player.X to JSON.Get("playerX")
  Action: Set Player.Y to JSON.Get("playerY")
  Action: Set global Score to JSON.Get("score")

Step 3: Use custom IDs instead of UIDs.

// Add an instance variable "EntityID" (number) to every object
// that needs persistent references.

// On creation, assign a unique ID:
Event
  Condition: Enemy → On created
  Action: Enemy → Set EntityID to global NextEntityID
  Action: System → Add 1 to NextEntityID

// When targeting, store the EntityID, not the UID:
Event
  Condition: Projectile → On created
  Action: Projectile → Set TargetEntityID to ClosestEnemy.EntityID

// To find the target after load:
Event
  Condition: System → For each Projectile
  Sub-event
    Condition: Enemy → EntityID = Projectile.TargetEntityID
    Action: Projectile → Move toward (Enemy.X, Enemy.Y)

Step 4: Build robust JSON with versioning.

// Saving with version number:
Event
  Condition: Button → On "Save" clicked
  Action: JSON → Clear
  Action: JSON → Set path ".version" to 2
  Action: JSON → Set path ".health" to Player.Health
  Action: JSON → Set path ".score" to global Score
  Action: JSON → Set path ".level" to global CurrentLevel
  Action: JSON → Set path ".playerX" to Player.X
  Action: JSON → Set path ".playerY" to Player.Y
  Action: LocalStorage → Set item "savegame" to JSON.ToCompactString

// Loading with version check:
Event
  Condition: LocalStorage → On item "savegame" get
  Action: JSON → Parse LocalStorage.ItemValue
  Sub-event
    Condition: JSON.Get(".version") >= 2
    Action: Set Player.Health to JSON.Get(".health")
    Action: Set global Score to JSON.Get(".score")
    Action: Set global CurrentLevel to JSON.Get(".level")
  Sub-event
    Condition: System → Else
    Action: Browser → Log "Old save format, starting fresh"
    Action: System → Restart layout

Step 5: Validate JSON before parsing (JavaScript).

// In a script block, safely parse save data:
try {
  const saveData = JSON.parse(localStorageValue);
  if (!saveData || typeof saveData.version !== "number") {
    console.warn("Invalid save data structure");
    return;
  }
  runtime.globalVars.Score = saveData.score ?? 0;
  runtime.globalVars.CurrentLevel = saveData.level ?? 1;
} catch (e) {
  console.error("Failed to parse save data:", e.message);
  // Corrupted save - start fresh or show error to player
}

Why This Works

Waiting for "On load complete" ensures you only read state after the engine has finished deserializing the entire save snapshot and recreating all objects. The load process involves destroying all current instances, parsing the save data, and recreating instances with their saved state. This takes at least one full frame.

Async-safe LocalStorage access respects the browser’s IndexedDB backend. LocalStorage in Construct 3 is not the synchronous window.localStorage — it uses an async key-value store. The "On item get" trigger fires after the browser has actually retrieved the data from disk.

Custom EntityIDs create stable references that survive serialization. UIDs are internal engine identifiers that can change when instances are destroyed and recreated. Your custom IDs are instance variables that get saved and restored along with all other instance data.

Savegame versioning prevents silent corruption. When your loading code checks the version, it can either migrate old data to the new format or cleanly reject it rather than partially loading an incompatible save and leaving the game in an undefined state.

"Every save/load bug I have ever seen comes down to the same thing: assuming something is synchronous when it is not. Save is async. Load is async. LocalStorage is async. Plan for it."

Related Issues

If your events do not trigger correctly after loading, see our guide on event sheet conditions not working. For physics objects that fall through the floor after loading (because physics state was not fully restored), see physics objects falling through floor. If your multiplayer game needs to sync save state across peers, check multiplayer signaling connection issues for peer communication fundamentals.

Always version your saves. Your future self will thank you when you add new features.