Every Lua Metamethod Explained
Metamethods are what make Lua’s tables genuinely powerful. They’re the hooks that let you intercept and customize fundamental operations — addition, indexing, function calls, even garbage collection. If you’ve been using metatables as simple lookup tables, you’re only seeing half the picture.
This guide covers every metamethod in Lua 5.4 with real code you can run. By the end, you’ll understand exactly when each one fires and how to use them in practice.
how metamethods work
A metatable is just a regular Lua table. Some of its keys (like __add or __index) have special meaning to Lua. When you perform an operation on a table that has a matching metamethod, Lua calls that function instead of proceeding with its default behavior.
The distinction matters: metatable is the container, metamethod is the individual hook. A table can have a metatable without any metamethods at all (that’s useful for marking type or sharing behavior). But the metamethods are where the real customization lives.
Not all operations have metamethods. Only the ones listed in this article are recognized by Lua.
arithmetic metamethods
Lua has eight metamethods for arithmetic operations. Each fires when the corresponding operator is used on a table operand.
| Metamethod | Operator | Called as |
|---|---|---|
__add | + | mt.__add(a, b) |
__sub | - | mt.__sub(a, b) |
__mul | * | mt.__mul(a, b) |
__div | / | mt.__div(a, b) |
__mod | % | mt.__mod(a, b) |
__pow | ^ | mt.__pow(a, b) |
__idiv | // | mt.__idiv(a, b) |
__unm | unary - | mt.__unm(a) |
Binary operators check the left operand first. If it has the metamethod, Lua uses it. Only if the left operand lacks the metamethod does Lua check the right operand.
local mt = {
__add = function(a, b) return a.x + b.x, a.y + b.y end,
__mul = function(a, s) return a.x * s, a.y * s end,
__unm = function(a) return -a.x, -a.y end,
}
a Vector2 class example
Here’s a complete Vector2 type using several arithmetic metamethods:
local Vector2 = {}
Vector2.__index = Vector2
function Vector2.new(x, y)
return setmetatable({x = x, y = y}, Vector2)
end
function Vector2.__add(a, b)
return Vector2.new(a.x + b.x, a.y + b.y)
end
function Vector2.__mul(a, scalar)
return Vector2.new(a.x * scalar, a.y * scalar)
end
function Vector2.__eq(a, b)
return a.x == b.x and a.y == b.y
end
function Vector2.__tostring(v)
return string.format("(%g, %g)", v.x, v.y)
end
local a = Vector2.new(1, 2)
local b = Vector2.new(3, 4)
print(a + b) -- (4, 6)
print(a * 3) -- (3, 6)
print(-a) -- (-1, -2)
print(a == Vector2.new(1, 2)) -- true
Operator precedence works as you’d expect. In a + b * 2, Lua evaluates b * 2 first (calling __mul), then adds the result to a (calling __add). The metamethods themselves don’t change precedence rules.
comparison metamethods
Lua provides three comparison metamethods: __eq, __lt, and __le. The <= operator can use __le directly, but Lua also provides a fallback: if __lt is defined but __le is not, a <= b calls not (b < a). The same symmetric fallback applies in reverse (if __le is defined but __lt is not, a < b calls not (b <= a)).
local Int = {}
Int.__index = Int
function Int.new(n)
return setmetatable({n = n}, Int)
end
function Int.__lt(a, b)
return a.n < b.n
end
function Int.__le(a, b)
return a.n <= b.n
end
local x = Int.new(3)
local y = Int.new(7)
print(x < y) -- true
print(x <= y) -- true
print(x > y) -- false
the __eq gotcha
Here’s the detail that catches most people: __eq only fires when both operands share the same metatable reference. Identical metamethods on separate metatable objects won’t trigger it.
local mt1 = { __eq = function(a, b) return a.n == b.n end }
local mt2 = { __eq = function(a, b) return a.n == b.n end }
local a = setmetatable({n = 1}, mt1)
local b = setmetatable({n = 1}, mt2)
print(a == b) -- false! Different metatables, __eq not called
If you want cross-type equality checks, you need a custom function. __eq alone can’t handle it.
table access: __index and __newindex
These two metamethods control what happens when you read from or write to a table. They’re called the most in real Lua code.
__index fires when you read a key that doesn’t exist in the table. __newindex fires when you assign to a key that doesn’t exist.
local function readonly(t)
return setmetatable({}, {
__index = t,
__newindex = function()
error("cannot modify a readonly table", 2)
end,
})
end
local data = readonly({name = "Alice", age = 30})
print(data.name) -- Alice
data.name = "Bob" -- error: cannot modify a readonly table
Both can be a function or a fallback table. If __index is a table, Lua does the lookup in that table directly. If it’s a function, Lua calls the function.
lazy loading with __index
One pattern you’ll see often is using __index to implement lazy loading (populating values on first access):
local function lazy(t, loader)
return setmetatable({}, {
__index = function(self, k)
local v = loader(k)
rawset(self, k, v) -- cache the result
return v
end,
})
end
local data = lazy({}, function(key)
print("Loading " .. key)
return 100
end)
print(data.a) -- Loading a \n 100
print(data.a) -- 100 (cached, loader not called again)
Calling rawset inside __index is important. If you use regular assignment, it would trigger __newindex recursively and cause a stack overflow.
the __call metamethod
__call makes a table callable like a function. When you write t(...), Lua checks if t has __call and calls it with whatever arguments you passed.
This is how you build functors, decorators, and callable objects:
local function memoize(fn)
return setmetatable({}, {
__call = function(self, arg)
if self[arg] then
return self[arg]
end
local result = fn(arg)
self[arg] = result
return result
end,
})
end
local fib = memoize(function(n)
if n <= 1 then return n end
return fib(n - 1) + fib(n - 2)
end)
print(fib(10)) -- 55
print(fib(10)) -- 55 (cached)
Every time you call fib(...), Lua invokes __call. The table itself has no numeric index (it’s purely callable).
string and length operations
__concat for the .. operator
__concat fires when you use the concatenation operator with a table as either operand:
local Row = {}
Row.__index = Row
function Row.new(values)
return setmetatable({values = values}, Row)
end
function Row.__concat(a, b)
if getmetatable(a) == Row then
return table.concat(a.values, ", ") .. ", " .. tostring(b)
else
return tostring(a) .. ", " .. table.concat(b.values, ", ")
end
end
local r = Row.new({"apple", "banana", "cherry"})
print("Items: " .. r) -- Items: apple, banana, cherry
__len for the # operator
__len fires when you apply the length operator to a table:
local IntList = {}
IntList.__index = IntList
function IntList.new(...)
return setmetatable({...}, IntList)
end
function IntList.__len(t)
return #t
end
local nums = IntList.new(10, 20, 30, 40, 50)
print(#nums) -- 5
Without __len, # on a table with holes returns undefined behavior in Lua. Defining __len gives you deterministic length semantics for your custom type.
garbage collection: __gc and __close
Lua has two metamethods related to cleanup: __gc for finalizers and __close for scope-based cleanup.
__gc — the finalizer
__gc is called when Lua’s garbage collector is about to collect a table or userdata with this metamethod set. It runs exactly once, when the object is being destroyed.
There’s an important constraint: in pure Lua, __gc only works on userdata or tables set via the C API (with luaL_setmetatable). A plain Lua table created with setmetatable({}, mt) does not have its __gc called when collected. This trips up many people using FFI libraries.
In Lua 5.4, this restriction was relaxed. luaL_setmetatable and lua_setmetatable on tables now also support __gc.
-- This works in Lua 5.4+ via C API, but not in pure Lua for plain tables
local mt = {
__gc = function(obj)
print("Finalizer called for: " .. tostring(obj))
end,
}
For pure Lua, you can’t reliably use __gc on tables. The workaround is to store resources in userdata when you need guaranteed finalization.
__close: scope-based cleanup
__close is newer (Lua 5.4) and fires when exiting a to-be-closed variable’s scope. It takes an additional argument: the error (or nil) that caused the exit.
local function makeResource()
return setmetatable({}, {
__close = function(self, err)
print("Closing resource, reason: " .. tostring(err))
end,
})
end
local resource = makeResource()
-- when 'resource' goes out of scope (to-be-closed):
-- __close is called with the exit error
Mark a variable as to-be-closed by using the close attribute in Lua 5.4:
local resource <close> = makeResource()
When the block exits (normally or via error), __close fires immediately.
protecting your metatable with __metatable
Set __metatable to any value (even true) and Lua blocks setmetatable and getmetatable on that table. This is useful when you’re exposing a table to untrusted code and don’t want callers messing with its metatable.
local safe = {}
setmetatable(safe, {
__metatable = "protected",
__index = {value = 42},
})
print(safe.value) -- 42
print(getmetatable(safe)) -- protected
setmetatable(safe, {}) -- error: protected metatable
getmetatable returns the value of __metatable itself rather than the actual metatable, which is why setmetatable still fails (it’s checking the metatable’s metatable entry and finding “protected”, not a callable function).
a Fraction class example
Let’s tie together several metamethods in one complete example (a Fraction type that supports arithmetic, equality, and string conversion):
local Fraction = {}
Fraction.__index = Fraction
function Fraction.new(num, den)
local self = setmetatable({}, Fraction)
self.num = num
self.den = den
return self
end
function Fraction.__add(a, b)
local num = a.num * b.den + b.num * a.den
local den = a.den * b.den
return Fraction.new(num, den)
end
function Fraction.__mul(a, b)
return Fraction.new(a.num * b.num, a.den * b.den)
end
function Fraction.__eq(a, b)
return a.num * b.den == b.num * a.den
end
function Fraction.__tostring(f)
return string.format("%d/%d", f.num, f.den)
end
function Fraction:simplify()
local g = math.gcd(self.num, self.den)
self.num = self.num // g
self.den = self.den // g
end
local a = Fraction.new(1, 2)
local b = Fraction.new(1, 3)
print(a + b) -- 5/6
print(a * b) -- 1/6
print(a == Fraction.new(2, 4)) -- true (same value, different object)
Every operation here (addition, multiplication, equality, printing) goes through a metamethod. That’s the point. You define the behavior once on the type, and all the operators work naturally.
common pitfalls
__index fires on missing keys only. If a key exists in the table, Lua returns it directly without consulting __index. This matters when you want to intercept all reads, not just missing ones (use rawget with __index to check if the key exists before delegating).
__newindex doesn’t auto-populate. If you set __newindex but not __index, writing to a new key calls __newindex but the key never gets stored in the table. You need to handle the assignment yourself (usually via rawset).
Arithmetic metamethods don’t work on plain strings. "hello" + 2 raises an error in stock Lua. To enable it, you need to set a metatable on the global string table (a pattern used by some libraries but not default Lua behavior).
Chained lookups stop at the first hit. For a + b, if a has __add, that’s used even if b also has __add. The metamethod from the right operand is only consulted if the left operand has nothing.
__gc on plain tables doesn’t fire in pure Lua. This is the most reported “bug” in Lua. Use userdata or C API tables if you need finalizers.
see also
- /tutorials/metatables-intro/ — metatables basics for reference
- /tutorials/classes-with-metatables/ — building objects with metatables
- /tutorials/operator-overloading/ — more operator customization patterns
- /guides/lua-garbage-collection/ — how Lua’s GC works and
__gcdetails
Written
- File: sites/luaguides/src/content/tutorials/metamethods-deep-dive.md
- Words: ~1104
- Read time: 6 min
- Topics covered: Every metamethod in Lua 5.4: arithmetic, comparison, __index/__newindex, __call, __concat/__len, __gc/__close, __metatable, gotchas, Fraction example
- Verified via: Lua 5.4 reference manual (lua.org)
- Unverified items: none