Syntax
-- ready only after successful read AND validation
-- failed read → no gameplay writes and no saveExamples
Load a small versioned progress record
ModuleScript named LessonLoad in ServerScriptService of the separate test experience. Its nil result means loading failed or stored data was invalid. No session is created and nothing is written on either path. The schema is intentionally limited to a monotonic highest lesson.
local store = game:GetService("DataStoreService"):GetDataStore("LessonProgress_TEST_v1")
local LessonLoad = {}
function LessonLoad.load(userId)
local ok, raw = pcall(function()
return store:GetAsync("player:" .. tostring(userId))
end)
if not ok then return nil, "Read failed" end
if raw == nil then return {version = 1, highestLesson = 0} end
if type(raw) ~= "table" or raw.version ~= 1 then return nil, "Unsupported record" end
local highest = raw.highestLesson
if type(highest) ~= "number" or highest ~= highest or highest < 0 or highest > 32 or highest % 1 ~= 0 then
return nil, "Invalid progress"
end
return {version = 1, highestLesson = highest}
end
return LessonLoadFail closed instead of inventing a session
Script in ServerScriptService using LessonLoad above. This wiring shows loading only; it does not save or award progress. A player who leaves during the request receives no session. A loading failure ends the session rather than allowing unsaved progress.
local Players = game:GetService("Players")
local LessonLoad = require(script.Parent:WaitForChild("LessonLoad"))
local started, sessions = {}, {}
local function onPlayer(player)
if started[player] then return end
started[player] = true
local data = LessonLoad.load(player.UserId)
if player.Parent ~= Players then return end
if not data then
player:Kick("Progress could not be loaded. Please rejoin later.")
return
end
sessions[player] = {ready = true, data = data}
print("Loaded", player.Name, data.highestLesson)
end
Players.PlayerAdded:Connect(onPlayer)
Players.PlayerRemoving:Connect(function(player)
sessions[player], started[player] = nil, nil
end)
for _, player in Players:GetPlayers() do task.spawn(onPlayer, player) endBest practices
- Never write defaults after pcall returns false, or after validation rejects a record.
- Do not silently reinterpret an unknown schema version as a new player.
- Keep loading, ready, saving and failed states explicit; only trusted server actions may change a ready session.
At a glance
- Purpose
- Typed scripting and Roblox development
- File extension
- .luau
- Runs in
- Luau host; Roblox engine examples require Roblox Studio
- Usually used with
- Roblox APIs and Studio
Specifications & further reading
Related Luau documentation
DataStoreService persists data across sessions and is accessed by server scripts. Studio API access can reach real experience data. Use a separate test experience and test store names before enabling access; these examples are learning exercises, not a complete production persistence framework.UpdateAsync and concurrent writes
UpdateAsync lets a non-yielding callback transform the latest value. It may invoke that callback again after a conflict. This example saves a monotonic highest-lesson record with max; that merge rule is appropriate for progress that never decreases, not balances, inventories or resets.Inference, annotations and strict mode
Luau can infer types from expressions, while annotations document the boundary of a function or record. Strict mode reports more incomplete assumptions. Static checks help authors; runtime checks still protect values received from players or saved storage.Retries, autosave and shutdown limits
A save can fail after gameplay succeeded. Keep the ready session available for another attempt, retry only a bounded number of times and report unresolved failures. PlayerRemoving and BindToClose are opportunities to save, not guarantees of durability.