Skip to content
AgileLogics
5 min read

__index and __newindex

Prerequisites: Metatables and metamethods

__index runs when you read a key that isn't in the table; __newindex when you assign to a key that isn't there.

Default values

local defaults = {color = "black", size = 12}
local settings = setmetatable({size = 16}, {__index = defaults})

print(settings.size)
print(settings.color)
print(rawget(settings, "color"))
16
black
nil

When __index is a table, Lua looks the key up there. rawget reads the table directly, skipping metamethods.

Computed values

local squares = setmetatable({}, {
  __index = function(t, n)
    local value = n * n
    rawset(t, n, value)   -- cache it
    return value
  end,
})
print(squares[12])
print(rawget(squares, 12))
144
144

When __index is a function, it's called with the table and the key.

Read-only tables

local function readOnly(t)
  return setmetatable({}, {
    __index = t,
    __newindex = function(_, key)
      error("attempt to modify read-only field '" .. tostring(key) .. "'", 2)
    end,
  })
end

local Colors = readOnly({RED = "#f00", GREEN = "#0f0"})
print(Colors.RED)
local ok, err = pcall(function() Colors.BLUE = "#00f" end)
print(ok, err)
#f00
false	main.lua:12: attempt to modify read-only field 'BLUE'

The proxy table is empty, so every write goes through __newindex.

Catching typos in globals

setmetatable(_G, {
  __newindex = function(_, name) error("assignment to undeclared global '" .. name .. "'", 2) end,
  __index = function(_, name) error("undeclared global '" .. name .. "'", 2) end,
})

print(pcall(function() return undefinedVariable end))
print(pcall(function() counter = 1 end))
false	main.lua:6: undeclared global 'undefinedVariable'
false	main.lua:7: assignment to undeclared global 'counter'

_G is the table that holds all globals. This "strict mode" trick turns accidental globals into errors at runtime.