In short
A data store saves plain data only: nil, booleans, numbers, strings, buffers and tables made of those. The table you passed to SetAsync or returned from UpdateAsync holds something else, usually an Instance (a Tool, a Part, a Player) or a Roblox type such as Vector3, CFrame or Color3. Array or Dictionary in the message is a kind of table, not the bad value inside it. Save plain values instead, such as the tool's name or the three numbers of a position, and rebuild the objects when the data loads.
What the message means
104: Cannot store Array in data store. Data stores can only accept valid UTF-8 characters.
Other forms of the same message:
104: Cannot store Dictionary in data store. Data stores can only accept valid UTF-8 characters.
Roblox turns your table into JSON text before it saves it. Anything JSON cannot hold stops the save with error code 104. The message only says which kind of table Roblox could not save: Array for a list ({a, b, c}), Dictionary for a table with named keys. It never names the value that broke it, and that value can be several levels deep.
The UTF-8 part of the message matters in only one case: a string that is not valid text, such as bytes packed with string.pack. For Instances and Roblox types, ignore it.
Nothing is saved when this happens, so a player who leaves loses that session. Wrap every save in pcall and test a save in Studio before you ship.
Causes and fixes
Three things cause this error in practice. Each one below has the smallest code that triggers it, then the fix.
- The table holds Instances
- The table holds a Vector3, CFrame or Color3
- A string is not valid UTF-8 text
The table holds Instances
Saving an inventory as a list of the Tool objects themselves is the most common version. An Instance lives in the running server; there is no way to write it into JSON.
local DataStoreService = game:GetService("DataStoreService")
local Players = game:GetService("Players")
local store = DataStoreService:GetDataStore("Inventory")
Players.PlayerRemoving:Connect(function(player)
local backpack = player:FindFirstChildOfClass("Backpack")
if not backpack then
return
end
-- a list of Tool objects: 104: Cannot store Array in data store
store:SetAsync(tostring(player.UserId), backpack:GetChildren())
end)
Save something that names each object, usually its Name, and keep the real tools in ServerStorage. When the data loads, clone the tool with that name back into the backpack.
local DataStoreService = game:GetService("DataStoreService")
local Players = game:GetService("Players")
local store = DataStoreService:GetDataStore("Inventory")
Players.PlayerRemoving:Connect(function(player: Player)
local backpack = player:FindFirstChildOfClass("Backpack")
if not backpack then
return
end
local toolNames: { string } = {}
for _, item in backpack:GetChildren() do
if item:IsA("Tool") then
table.insert(toolNames, item.Name)
end
end
local ok, err = pcall(function()
store:SetAsync(tostring(player.UserId), toolNames)
end)
if not ok then
warn(`[SaveInventory] save failed for {player.UserId}: {err}`)
end
end)
When the player joins, load the list and clone ServerStorage.Tools[name] for each name that still exists. A name for a tool you have since removed is simply skipped.
The table holds a Vector3, CFrame or Color3
Saving where a player built something, or the colour they picked, puts Roblox data types into the table. They look like plain values in scripts, but they are not numbers, strings or tables, so serialization fails. The message then says Dictionary or Array, whichever kind of table they sat in.
local DataStoreService = game:GetService("DataStoreService")
local store = DataStoreService:GetDataStore("Plots")
local plot = {
position = Vector3.new(10, 0, -24),
color = Color3.fromRGB(255, 170, 0),
}
-- 104: Cannot store Dictionary in data store
store:SetAsync("plot_12345", plot)
Store each type as plain numbers and turn them back when you load: {x, y, z} for a position, the hex text from Color3:ToHex() for a colour. For a CFrame, save its twelve numbers from GetComponents(), or just the position and one angle if that is all your game needs.
local DataStoreService = game:GetService("DataStoreService")
local store = DataStoreService:GetDataStore("Plots")
local position = Vector3.new(10, 0, -24)
local color = Color3.fromRGB(255, 170, 0)
local saved = {
position = { position.X, position.Y, position.Z },
color = color:ToHex(), -- "FFAA00"
}
local ok, err = pcall(function()
store:SetAsync("plot_12345", saved)
end)
if not ok then
warn("[SavePlot] save failed:", err)
end
-- when loading:
local function readPlot(data: { position: { number }, color: string })
local p = data.position
return Vector3.new(p[1], p[2], p[3]), Color3.fromHex(data.color)
end
print(readPlot(saved))
A string is not valid UTF-8 text
Data packed into a string with string.pack, or text built byte by byte with string.char, can contain bytes that are not valid UTF-8. JSON only holds valid text, so this is the one case where the end of the message is the real reason.
local DataStoreService = game:GetService("DataStoreService")
local store = DataStoreService:GetDataStore("Builds")
-- three numbers packed into raw bytes, which are not valid text
local packed = string.pack("fff", 10.5, 0, -24.25)
-- 104: Cannot store Array in data store. Data stores can only accept valid UTF-8 characters.
store:SetAsync("build_12345", { packed })
Data stores accept buffer values, so pack binary data into a buffer instead of a string. Or keep the numbers as numbers: a table of numbers is often small enough.
local DataStoreService = game:GetService("DataStoreService")
local store = DataStoreService:GetDataStore("Builds")
local data = buffer.create(12)
buffer.writef32(data, 0, 10.5)
buffer.writef32(data, 4, 0)
buffer.writef32(data, 8, -24.25)
local ok, err = pcall(function()
store:SetAsync("build_12345", data)
end)
if not ok then
warn("[SaveBuild] save failed:", err)
end
Fix it in your place
Paste the error, RoCode fixes it in your place
Copy the line from Output and paste it here. RoCode opens the script it names in the place you have open in Studio, works out which of the causes above it is, and edits the code there instead of handing you a snippet to paste.
RoCode is an AI agent for Roblox Studio: it reads the scripts in the place you have open, finds the line and edits it there, and every change is an undo point. It runs on Windows or macOS with a desktop browser. Free to start, with free credits every day.
How to find the line that causes it
- The 104 error is raised by the
SetAsyncorUpdateAsynccall. Click the red line in Output to open that script there. - Just above the save, add
print(game:GetService("HttpService"):JSONEncode(yourTable)). Values that come out asnullor are missing from the printed text are the ones a data store cannot save. If the print line itself errors, the table holds something JSON cannot hold at all. - Walk through the table and print
typeof(value)for every value. Anything that printsInstance,Vector3,CFrame,Color3,EnumItemorfunctionis what the data store refuses.
Questions
Can a Roblox data store save a table?
Yes, as long as everything in it is a boolean, a number, a string, a buffer or another such table. Tables inside tables are fine. What fails is an Instance or a Roblox type such as Vector3 anywhere inside it.
How do I save a Vector3 or CFrame in a data store?
Save its numbers. A Vector3 becomes {x, y, z}; a CFrame becomes the twelve numbers from GetComponents(), which CFrame.new takes back in the same order. Turn them back into the type when the data loads.
How do I save a player's tools or items?
Save the names (or your own item IDs) in a list. Keep one copy of each tool in ServerStorage and clone it into the backpack when the player joins and their data loads.
Why does it say Array when my table has keys?
The table that failed can be one inside yours, such as a list of items inside a player's record. Check every level of the table, not just the top.
Can RoCode fix it?
Yes. Paste the Output line into RoCode. It reads the save script in the place you have open, finds the Instance or Roblox type in the saved table and rewrites the save and the load to use plain values.
Sources and further reading
- Roblox Creator Docs: data store serialization. the Serialization section lists the supported types
- Roblox Creator Docs: data store error codes. code 104, CantStoreValue
- Roblox Creator Docs: buffer library
How the code was checked: every block marked Fixed passes Luau's strict type checker against Roblox's API definitions (luau-lsp, 4 November 2026), and every block marked Breaks compiles and fails only when it runs. The message wording was checked against the message strings in Roblox Studio itself (macOS build installed on 24 October 2026) on 4 November 2026. The list of values a data store accepts is from the Serialization section of the Roblox data store docs. A type check catches misspelt APIs and wrong types, not game logic, so playtest in Studio before you publish.