Skip to main content

Metatables & Metamethods

Metatables let you customize the behavior of tables (and userdata). Every table can have one metatable. Metamethods are fields with __ prefix that define behavior for operations.


Setting metatables

local t = {}
local mt = {}
setmetatable(t, mt)

-- Get metatable
local mt = getmetatable(t)

-- Protected get (won't trigger __metatable)
local mt = rawgetmetatable(t)

[!tip] __metatable field If a metatable has __metatable, getmetatable() returns that value instead. Protects metatables from casual access.


Metamethod list

Arithmetic

MetamethodOperationNotes
__adda + b
__suba - b
__mula * b
__diva / b
__moda % b
__powa ^ b
__unm-aunary minus
__idiva // bfloor division
__banda & bbitwise AND
__bora | bbitwise OR
__bxora ~ bbitwise XOR
__bnot~abitwise NOT
__shla << bleft shift
__shra >> bright shift
__concata .. bconcatenation

Relational

MetamethodOperationNotes
__eqa == bonly if same metamethod
__lta < bmust define < not >
__lea <= bmust define <= not >=

[!note] __eq only fires when both operands share the same metamethod function. Otherwise → false.

Table access

MetamethodOperation
__indexRead missing key
__newindexWrite to missing key
__len#t length
__callt(args) function call
__gcGarbage collection finalizer
__closeVariable goes out of scope (<close> attribute)
__modeWeak references ("k", "v", "kv")
__tostringtostring(t)

__index — reading missing keys

When you read a key that's nil in a table, LuaLuaA lightweight, embeddable scripting language used as the primary scripting language in FiveM server development. checks __index.

local proto = {hp = 100, mp = 50}
local mt = { __index = proto }
local player = setmetatable({}, mt)

print(player.hp) -- 100 (from proto)
print(player.mp) -- 50 (from proto)

__index as function

local mt = {
__index = function(t, key)
if key == "upper" then
return string.upper
end
end
}
local t = setmetatable({}, mt)
print(t.upper("hello")) -- HELLO

Prototype chain

local base = {x = 1}
local mid = setmetatable({y = 2}, {__index = base})
local top = setmetatable({z = 3}, {__index = mid})

print(top.x) -- 1 (follows chain: top → mid → base)
print(top.y) -- 2
print(top.z) -- 3

__newindex — writing missing keys

Fires when you assign to a key that doesn't exist in the table.

local mt = {
__newindex = function(t, key, value)
print("Setting " .. tostring(key) .. " = " .. tostring(value))
rawset(t, key, value) -- actually set it
end
}
local t = setmetatable({}, mt)
t.x = 10 -- prints: Setting x = 10

[!warning] __newindex only fires for new keys If the key already exists, the assignment is direct. Use rawset to bypass metamethods.


__call — callable tables

local mt = {
__call = function(t, ...)
print("Called with:", ...)
end
}
local f = setmetatable({}, mt)
f("hello", 42) -- Called with: hello 42

__tostring — string representation

local mt = {
__tostring = function(t)
return string.format("Point(%d, %d)", t.x, t.y)
end
}
local p = setmetatable({x = 3, y = 4}, mt)
print(p) -- Point(3, 4)

__gc — finalizers

local mt = {
__gc = function(t)
print("Goodbye!", t.name)
end
}
local o = setmetatable({name = "obj"}, mt)
o = nil
collectgarbage() -- prints: Goodbye! obj

See 10-Garbage-Collection for details.


__close — to-be-closed variables (5.4)

local mt = {
__close = function(t, err)
print("Closing:", t.name, err or "ok")
end
}

do
local handle <close> = setmetatable({name = "file"}, mt)
-- work with handle
end -- __close called here

__len — length operator

local mt = {
__len = function(t)
return t._count or 0
end
}
local t = setmetatable({_count = 42}, mt)
print(#t) -- 42

__eq, __lt, __le — comparisons

local mt = {
__eq = function(a, b) return a.value == b.value end,
__lt = function(a, b) return a.value < b.value end,
__le = function(a, b) return a.value <= b.value end,
}

local a = setmetatable({value = 1}, mt)
local b = setmetatable({value = 2}, mt)
print(a < b) -- true
print(a == b) -- false

Raw access (bypass metamethods)

rawget(t, key) -- read without __index
rawset(t, key, value) -- write without __newindex
rawequal(a, b) -- compare without __eq
rawlen(t) -- length without __len

Proxy table pattern

Use __index and __newindex together to intercept all access:

local data = {} -- real storage
local proxy = {}

local mt = {
__index = function(_, key)
print("GET", key)
return data[key]
end,
__newindex = function(_, key, value)
print("SET", key, value)
data[key] = value
end,
__pairs = function(_)
return pairs(data)
end
}

setmetatable(proxy, mt)
proxy.x = 10 -- SET x 10
print(proxy.x) -- GET x → 10

OOP with metatables

local Player = {}
Player.__index = Player

function Player.new(name, hp)
local self = setmetatable({}, Player)
self.name = name
self.hp = hp or 100
return self
end

function Player:damage(n)
self.hp = self.hp - n
if self.hp <= 0 then
print(self.name .. " died!")
end
end

function Player:__tostring()
return self.name .. " (HP: " .. self.hp .. ")"
end

-- Inheritance
local Warrior = setmetatable({}, {__index = Player})
Warrior.__index = Warrior

function Warrior.new(name)
local self = Player.new(name, 150) -- warriors have more HP
return setmetatable(self, Warrior)
end

local w = Warrior.new("Thor")
w:damage(10)
print(w) -- Thor (HP: 140)