In short
The table you tried to change is frozen: read-only for good. Usually a ModuleScript froze its settings with table.freeze, and another script then tried to change a value, add an item or sort it. It also happens when you copy a frozen table with table.clone but change a table nested inside it, and when you add your own function to a built-in library such as string or table. A frozen table cannot be unfrozen: change a copy, or keep the values that change in a separate, unfrozen table.
What the message means
ServerScriptService.Shop:11: attempt to modify a readonly table
table.freeze(t) makes t read-only, which is a good way to protect constants such as item prices. After that, setting a key, table.insert, table.remove, table.sort, table.clear, rawset and setmetatable on it all fail with this message. table.isfrozen(t) tells you whether a table is frozen.
Freezing is shallow: it freezes that table only, not the tables inside it, unless those were frozen too. Roblox's own libraries (string, table, math and the rest) are read-only in the same way.
Causes and fixes
Three things cause this error in practice. Each one below has the smallest code that triggers it, then the fix.
- A ModuleScript returns a frozen table
- A copy still holds a frozen table inside it
- Code adds a function to string, table or math
A ModuleScript returns a frozen table
A settings module that ends with return table.freeze(Config) protects its values on purpose. A script that then tries to change one, for a sale or a test, hits the freeze.
local Prices = table.freeze({
Sword = 100,
Shield = 150,
})
-- a weekend sale: attempt to modify a readonly table
Prices.Sword = 50
print(Prices.Sword)
Leave the frozen table as the source of truth and keep anything that changes during the game in its own table. Read the changing value first and fall back to the frozen one.
local Prices = table.freeze({
Sword = 100,
Shield = 150,
})
local saleDiscount: { [string]: number } = {} -- not frozen: changes during the game
saleDiscount.Sword = 0.5
local function priceOf(item: string): number
local base = (Prices :: { [string]: number })[item] or 0
return math.floor(base * (saleDiscount[item] or 1))
end
print(priceOf("Sword")) --> 50
A copy still holds a frozen table inside it
table.clone returns a new, unfrozen table, but it copies only the top level. Tables inside it are the same tables as before, so if they were frozen, they still are. Starting each player from a frozen template is where this shows up.
local Template = table.freeze({
coins = 0,
inventory = table.freeze({ "Starter Sword" }),
})
local data = table.clone(Template)
data.coins = 25 -- fine: the copy is not frozen
-- data.inventory is still the frozen list: attempt to modify a readonly table
table.insert(data.inventory, "Shield")
Copy every level. A short deep copy function clones each nested table, so the player gets tables of their own that can change.
local Template = table.freeze({
coins = 0,
inventory = table.freeze({ "Starter Sword" }),
})
local function deepCopy<T>(original: T): T
if type(original) ~= "table" then
return original
end
local copy = {}
for key, value in original :: { [any]: any } do
copy[key] = deepCopy(value)
end
return (copy :: any) :: T
end
local data = deepCopy(Template)
data.coins = 25
table.insert(data.inventory, "Shield")
print(data.inventory)
Code adds a function to string, table or math
Older Lua tutorials add helpers straight onto the built-in libraries, such as function string.trim(s). In Roblox those libraries are read-only, so the line that adds the helper fails.
-- attempt to modify a readonly table
function string.trim(s: string): string
return (string.gsub(s, "^%s*(.-)%s*$", "%1"))
end
print(string.trim(" hello "))
Put your helpers in your own table, ideally in a ModuleScript that other scripts can require.
local StringUtil = {}
function StringUtil.trim(s: string): string
return (string.gsub(s, "^%s*(.-)%s*$", "%1"))
end
print(StringUtil.trim(" hello "))
A variable you named table or string hides the library of that name in the rest of the script. Pick another name, such as list, so calls like table.insert still reach the real library.
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
- Open Output (Window menu, or the Script tab), clear it with Ctrl+K (⌘K on Mac), and press Play.
- Find the first red line. It starts with the script's place in the Explorer and the line number, such as
ServerScriptService.Shop:12:. - Click the red line. Studio opens that script with the cursor on the failing line.
- Read the lines between
Stack BeginandStack Endunder it: they list the calls that led there, newest first, so you can see which function called the broken one. - Still not sure which value is wrong? Add
print(typeof(x), x)just above the line, or set a breakpoint on it and read the values in the Watch window.
Questions
How do I unfreeze a table in Roblox?
You cannot. Freezing is permanent. Make a copy with table.clone (or a deep copy for nested tables) and change the copy.
How do I check whether a table is frozen?
Call table.isfrozen(t). It returns true for a frozen table.
Should I freeze my config tables?
For values that must not change while the game runs, such as prices and drop rates, yes: a mistaken write fails loudly instead of quietly changing the game. Keep the values that do change in separate tables.
Can RoCode fix it?
Yes. Paste the Output line into RoCode. It reads the scripts in the place you have open, finds which frozen table the line writes to and changes the code to use a copy or a separate table.
Sources and further reading
- Roblox Creator Docs: tables (freeze tables). shallow and deep freezes; freezing is permanent
- Roblox Creator Docs: table library. freeze, isfrozen and clone
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, and each cause was reproduced with the Luau command line tool the same day. A type check catches misspelt APIs and wrong types, not game logic, so playtest in Studio before you publish.