luaguides

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.

MetamethodOperatorCalled 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)
__unmunary -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

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