Roblox Studio error

attempt to modify a readonly table

What attempt to modify a readonly table means in Roblox Studio, the three things that cause it with the code that triggers each, and the fix for each one.

Updated 4 November 20264 min readErrorLuauWritten by

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.

On this page
  1. What it means
  2. Causes and fixes
  3. Fix it with RoCode
  4. Find the line
  5. Questions
  6. Sources

What the message means

Output (example)
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 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.

Breaks Script in ServerScriptService named Shop
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.

Fixed Script in ServerScriptService named Shop
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.

Breaks Script in ServerScriptService named PlayerData
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.

Fixed Script in ServerScriptService named PlayerData
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.

Breaks Script in ServerScriptService named Util
-- 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.

Fixed Script in ServerScriptService named Util
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.

For example: ServerScriptService.Shop:11: attempt to modify a readonly table

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

  1. Open Output (Window menu, or the Script tab), clear it with Ctrl+K (⌘K on Mac), and press Play.
  2. Find the first red line. It starts with the script's place in the Explorer and the line number, such as ServerScriptService.Shop:12:.
  3. Click the red line. Studio opens that script with the cursor on the failing line.
  4. Read the lines between Stack Begin and Stack End under it: they list the calls that led there, newest first, so you can see which function called the broken one.
  5. 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

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.

More Roblox errors

All Roblox Studio errors

Stop hunting errors line by line.

RoCode reads the scripts in your open place, fixes the error where it happens and tells you what it changed. Free every day, no card required.