Quick answer: Construct 3’s System Save/Load actions are asynchronous. You must use the On save complete and On load complete triggers before acting on save data. Other causes include objects not being present in the layout at save time, not persisting saves to Local Storage, and dynamic objects that are not recreated before loading.
You call System Save, then System Load, and the game does not return to the saved state. Positions are wrong, instance variables are reset, dynamically created objects are missing, or the load appears to do nothing at all. The save/load system in Construct 3 is powerful but has several non-obvious requirements. Missing any one of them causes silent failures with no error messages.
The Symptom
You trigger a save action (for example, when the player presses a key). Later, you trigger a load action. One or more of these things happens:
The game appears completely unchanged after loading — as if the load did nothing. Objects are in their current positions rather than their saved positions. Instance variables show current values, not saved values. Dynamically created objects (spawned via Create Object at runtime) are gone after loading. Global variables are reset to their initial values. Or the game partially loads — some objects restore correctly but others do not.
No error messages appear in the console. The On load complete trigger may or may not fire, depending on the specific failure mode.
What Causes This
There are five primary causes, each related to a different aspect of how Construct 3’s save system works under the hood:
1. Treating save/load as synchronous. This is the most common cause. System Save and System Load are asynchronous operations. When you call System > Save, the save does not happen instantly on that line. It completes some time later, and you must wait for the On save complete trigger. If you try to load immediately after saving (without waiting for completion), you may load an empty or incomplete save state.
2. Save data not persisted to Local Storage. By default, System Save stores data in memory only. If the user closes the browser tab or refreshes the page, the save data is lost. Many developers assume the save persists automatically, but you must explicitly write the save data to Local Storage if you want it to survive across sessions.
3. Dynamic objects missing at load time. System Save captures all object instances present in the layout at the time of the save, including dynamically created ones. When you load, Construct 3 recreates the saved state. However, if the object type itself is not referenced anywhere in the layout (for example, a bullet type that is only created dynamically), the engine may not have the type loaded and cannot recreate the instances. At least one instance of each dynamic type must exist in the layout, or the type must be referenced elsewhere.
4. Loading on a different layout. System Save captures the state of the current layout. If you save on Layout 1 and load on Layout 2, the behavior is undefined. Some objects may restore, but layout-specific state like scroll position, layer properties, and non-global objects from Layout 1 will not apply correctly to Layout 2.
5. Global variables not included in save scope. By default, System Save captures all instance data and global variables. However, if you have modified what the save captures through the “Save/Load” property on specific object types (setting them to not save), those objects are excluded. Additionally, some plugins and behaviors have properties that are not serialized by the save system.
The Fix
Step 1: Always use async callbacks. Never assume save or load has completed until the corresponding trigger fires.
// WRONG - treating save/load as synchronous
Keyboard: On F5 pressed
→ System: Save to slot "save1"
→ TextUI: Set text to "Game saved!"
// This message shows BEFORE the save completes!
// RIGHT - using the completion trigger
Keyboard: On F5 pressed
→ System: Save to slot "save1"
System: On save complete
→ TextUI: Set text to "Game saved!"
System: On save failed
→ TextUI: Set text to "Save failed!"
Step 2: Persist save data to Local Storage. Combine System Save with the Local Storage plugin to make saves survive page reloads.
// Save: write to slot, then persist to Local Storage
Keyboard: On F5 pressed
→ System: Save to slot "save1"
System: On save complete
→ LocalStorage: Set item "save1"
to System.SaveStateJSON
// SaveStateJSON contains the full serialized state
LocalStorage: On item set "save1"
→ TextUI: Set text to "Game saved to storage!"
// Load: read from Local Storage, then load from slot
Keyboard: On F9 pressed
→ LocalStorage: Get item "save1"
LocalStorage: On item get "save1"
→ System: Load from JSON LocalStorage.ItemValue
System: On load complete
→ TextUI: Set text to "Game loaded!"
Step 3: Ensure dynamic object types are available. If your game creates objects at runtime (bullets, collectibles, procedural terrain), make sure the object types are known to the engine at load time.
// Method A: Place one instance of each dynamic type
// in the layout, off-screen, at design time.
// Position it at (-1000, -1000) so it is never visible.
// The save system needs the type to be registered.
// Method B: Create and immediately destroy in On Start
System: On start of layout
→ System: Create object Bullet on layer "Game"
at (-1000, -1000)
→ Bullet: Destroy
// Type is now registered, even though instance is gone
// Note: destroy AFTER the create completes
// Method C: Use a container or family that includes all types
// This ensures the types are always loaded with the layout
Step 4: Always save and load on the same layout. If your game has multiple layouts, save the layout name as part of your save data and navigate to the correct layout before loading.
// Save the current layout name alongside the state
System: On save complete
→ LocalStorage: Set item "save1_layout"
to LayoutName
// When loading, go to the correct layout first
LocalStorage: On item get "save1_layout"
→ System: Go to layout LocalStorage.ItemValue
// Then load the state on the new layout's start
// Use a global variable to signal "load pending"
System: On start of layout
System: LoadPending = 1
→ System: Set LoadPending to 0
→ LocalStorage: Get item "save1"
Step 5: Use JSON-based saving for selective data. When you only need to save specific data (player stats, inventory, progress flags), JSON serialization gives you full control.
// Scripting API - custom JSON save
function saveGameData(runtime) {
const player = runtime.objects.Player.getFirstInstance();
const saveData = {
version: 1,
player: {
x: player.x,
y: player.y,
health: player.instVars.Health,
score: player.instVars.Score,
inventory: player.instVars.Inventory
},
globals: {
level: runtime.globalVars.CurrentLevel,
difficulty: runtime.globalVars.Difficulty,
timePlayed: runtime.globalVars.TimePlayed
},
timestamp: Date.now()
};
const json = JSON.stringify(saveData);
localStorage.setItem("mygame_save1", json);
console.log("Saved:", json.length, "bytes");
}
function loadGameData(runtime) {
const json = localStorage.getItem("mygame_save1");
if (!json) {
console.log("No save found");
return false;
}
const saveData = JSON.parse(json);
const player = runtime.objects.Player.getFirstInstance();
player.x = saveData.player.x;
player.y = saveData.player.y;
player.instVars.Health = saveData.player.health;
player.instVars.Score = saveData.player.score;
runtime.globalVars.CurrentLevel = saveData.globals.level;
return true;
}
Step 6: Implement save slot management. For games with multiple save slots, keep an index of available saves.
// Manage multiple save slots with metadata
function getSaveSlots() {
const index = localStorage.getItem("mygame_index");
return index ? JSON.parse(index) : {};
}
function saveToSlot(slotNum, runtime) {
const saveData = buildSaveData(runtime);
const key = "mygame_slot" + slotNum;
localStorage.setItem(key, JSON.stringify(saveData));
// Update the slot index with metadata
const index = getSaveSlots();
index[slotNum] = {
date: new Date().toISOString(),
level: runtime.globalVars.CurrentLevel,
playTime: runtime.globalVars.TimePlayed
};
localStorage.setItem("mygame_index", JSON.stringify(index));
}
Why This Works
The save/load system failures come down to timing and scope:
Async callbacks are necessary because serializing the entire game state is not instantaneous. Construct 3 serializes all object positions, instance variables, behavior states, global variables, and more. This process yields to the browser event loop, which means code after the Save action runs before the save is actually complete. The On save complete trigger fires only when serialization is fully done.
Local Storage persistence bridges the gap between in-memory snapshots and permanent storage. System Save creates a snapshot in JavaScript memory, which is cleared when the page unloads. Writing that snapshot to Local Storage puts it in the browser’s persistent key-value store, which survives page reloads, tab closes, and browser restarts (barring private browsing mode).
Dynamic object registration matters because Construct 3 needs to know the full definition of an object type (its behaviors, instance variables, animation frames) to recreate instances during load. If the type was never instantiated or referenced in the current layout, the engine may not have loaded its definition from the project data.
JSON-based saving gives you explicit control over what is serialized. System Save captures everything, which can be both too much (large save files) and too little (some plugin states are not serializable). Manual JSON serialization lets you pick exactly what matters for your game’s continuity.
"System Save is a snapshot, not a database. It captures one moment in time. If you need selective, structured, queryable save data, build your own JSON serialization. It is more work upfront but vastly easier to debug and extend."
Related Issues
If your For Each loops that process saved data are skipping instances, see Fix: Construct 3 For Each Loop Skipping Instances. For save/load data transmitted via AJAX that fails with network errors, check Fix: Construct 3 AJAX Request CORS Error. If loaded objects with Platform behavior fall through the ground, see Fix: Construct 3 Platform Behavior Falling Through Platforms. And if particle effects are missing after loading a save on mobile, see Fix: Construct 3 Particles Not Showing on Mobile.
Save is async. Always wait for On save complete.