Persist LÖVE Game State with love.filesystem
Persist LÖVE game state across sessions by writing validated JSON to the platform save directory with love.filesystem, then falling back to defaults when the file is missing or corrupt.
15 Feb 2026, 01:32 UTC

Problem and takeaway
A LÖVE game that keeps player progress only in memory loses it when the window closes. Writing next to the executable is unreliable and often blocked. The useful approach is to serialize a plain Lua table, write it to the platform save directory with love.filesystem.write, read it back with love.filesystem.read, and treat any missing or unreadable file as a signal to start from defaults.
Desired outcome
On launch, the game loads a saved state table if a valid file exists. On exit or checkpoint, it writes the current state to a file inside love.filesystem.getSaveDirectory(). If the file is missing, corrupt, or unwritable, the game continues with default values instead of crashing.
Prerequisites
- LÖVE 0.10.x or 11.x.
love.filesystem.getInfoexists in 0.10 and later; older releases uselove.filesystem.exists. - A Lua table that contains only plain values: strings, numbers, booleans, and nested tables of the same kind. Functions, coroutines, threads, and userdata cannot be serialized.
- A JSON serializer bundled with your game. LÖVE does not include one by default.
Procedure
1. Choose a save filename and defaults
Use a relative filename only. Never hardcode an absolute path or assume the current working directory is writable.
local SAVE_FILE = "savegame.json"
local DEFAULTS = {
level = 1,
score = 0,
unlocked_levels = {1},
volume = 0.7,
}
2. Load state safely
pcall protects the game from halting if the saved file contains invalid JSON or unexpected data. love.filesystem.getInfo checks whether the file exists before attempting to read it.
local json = require("json") -- bundle a JSON module with your project
local function deep_equal(a, b)
if type(a) ~= type(b) then return false end
if type(a) ~= "table" then return a == b end
for k, v in pairs(a) do
if not deep_equal(v, b[k]) then return false end
end
for k, v in pairs(b) do
if a[k] == nil then return false end
end
return true
end
local function load_state()
local info = love.filesystem.getInfo(SAVE_FILE)
if not info then
return DEFAULTS
end
local data, read_err = love.filesystem.read(SAVE_FILE)
if not data then
print("Save read failed: " .. tostring(read_err))
return DEFAULTS
end
local ok, decoded = pcall(json.decode, data)
if not ok or type(decoded) ~= "table" then
print("Save file is corrupt or invalid; resetting to defaults")
return DEFAULTS
end
return decoded
end
3. Save state safely
Serialize the table first, then write the string. Check the write result so a full disk or read-only save directory does not go unnoticed.
local function save_state(state)
local serialized, encode_err = json.encode(state)
if not serialized then
print("Save serialization failed: " .. tostring(encode_err))
return false
end
local ok, write_err = love.filesystem.write(SAVE_FILE, serialized)
if not ok then
print("Save write failed: " .. tostring(write_err))
return false
end
return true
end
4. Wire it into LÖVE callbacks
local state = DEFAULTS
function love.load()
state = load_state()
print("Save directory: " .. love.filesystem.getSaveDirectory())
end
function love.keypressed(key)
if key == "s" then
state.level = state.level + 1
state.score = state.score + 100
if not save_state(state) then
print("Progress was not saved")
end
end
end
Expected checks
love.filesystem.getSaveDirectory()returns a non-empty string at startup.- A test write to a temporary file succeeds in that directory.
- Writing a known table, reading it back, and decoding it produces a table equal to the original.
- Deleting the save file and relaunching loads
DEFAULTSwithout error.
Recovery and rollback
Because love.filesystem.write changes state by creating or replacing a file, a failed or corrupt save should not leave the game in a broken state. Recovery options:
- If decoding fails, ignore the file and return
DEFAULTS. - Optionally delete the corrupt file so the next launch starts cleanly.
- Log the error to the console but do not halt the game.
- If a write fails, keep the previous state in memory and retry later rather than overwriting the good save with partial data.
Limitations
- The save directory is sandboxed and differs by operating system. Typical locations include the application support folder on macOS,
%APPDATA%/LOVE on Windows, and~/.local/share/loveon Linux, but you should readlove.filesystem.getSaveDirectory()at runtime instead of hardcoding a path. - LÖVE does not include a JSON serializer, so you must bundle one and verify it handles your table structure.
- Only plain values survive serialization. Strip functions, userdata, and coroutines before saving, or the encoded file will be incomplete or invalid.
love.filesystem.writecan fail on read-only or sandboxed systems. Always check its return value.
Practical verification
- Launch the game from a terminal in your project folder using
love .. No special permissions are required; run as a normal user. - Confirm the console prints a non-empty save directory.
- Trigger a save, close the game, relaunch, and confirm the saved values reappear.
- Delete
savegame.jsonfrom the save directory, relaunch, and confirm the game falls back to defaults without errors.
If any check fails, inspect the console for serialization or write errors and verify the save directory is writable before proceeding.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.