luaguides

Every Lua Metamethod Explained

Metamethods are what make Lua’s tables genuinely powerful. A Lua metamethod is a hook that intercepts a fundamental operation — addition, indexing, function calls, even garbage collection — and lets you redefine what it means for your own types. 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.

prerequisites

You should be comfortable with Lua tables, including creating them, indexing them, and understanding that table keys can be functions. You’ll also need basic knowledge of metatables (setmetatable and getmetatable), since every metamethod lives inside one. Familiarity with function syntax and Lua’s colon notation for method calls will help, as most examples define metamethods as functions on a shared metatable.

how lua 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.

When Lua encounters an operation, it checks the left operand’s metatable first. If the matching metamethod exists there, Lua calls it. Otherwise it checks the right operand’s metatable. This left-to-right priority means your custom types behave predictably in mixed expressions: the type on the left determines which metamethod runs.

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. The table below lists every arithmetic metamethod, the operator it binds to, and the exact call signature Lua uses; the left operand is always passed first, and binary operators receive both operands as arguments.

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,
}

The example above shows an anonymous metatable with three arithmetic metamethods. The Vector2 class below ties these together into a reusable type. The __add metamethod covers vector addition, __mul handles scalar multiplication, and __unm manages negation. Operator precedence works as you’d expect inside expressions like a + b * 2, where __mul fires before __add because multiplication binds tighter. Every operator call goes through the type’s metatable, so Vector2.new(1,2) + Vector2.new(3,4) is a function call behind the scenes.

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 __lt/__le fallback works well for same-type comparisons. But equality has a stricter rule that surprises many developers: __eq only fires when both operands point to the exact same metatable object in memory. Two separate tables that happen to define identical __eq functions are different references, so Lua falls through to the default pointer-equality check. This means you cannot make Vector == Fraction work through __eq alone; cross-type equality comparisons require a purpose-built function instead.

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 it, and that function form opens up the lazy loading pattern. By computing values only when a key is first accessed and then storing the result with rawset, you avoid computing expensive data upfront while still getting O(1) access on subsequent reads. The rawset call is essential because a normal assignment inside __index would hit __newindex, which on a table without one is harmless, but on a wrapped or guarded table causes infinite recursion or errors.

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)

Beyond __index, Lua also lets you make tables behave like functions through __call. This turns a table into a functor: an object you invoke with parentheses like table(args). The table can double as its own storage, holding cached results alongside the callable interface. The memoization example below stores computed Fibonacci values directly in the same table that fib(10) dispatches through, so every invocation both reads from and writes to the same structure.

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 stores cached Fibonacci values keyed by the integer argument, and __call handles both lookup and computation. This self-caching pattern works because __call receives the table as its first argument (self), so the function can read from and write to its own storage naturally.

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. Unlike __concat which triggers on either operand, __len only fires on the table the # operator is applied to directly; there is no fallback to a second operand. This makes __len simpler to reason about but also means your custom length definition must handle the table’s internal representation without any help from surrounding context.

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, Lua’s # operator behaves unpredictably on tables that contain gaps in their integer keys; the result may be any boundary between a sequence of contiguous numeric indices and a hole. Defining __len replaces that internal boundary-search with your own logic, giving you deterministic length semantics. This is useful for types that track their size separately from the table contents, like ring buffers or sparse structures where # on the raw table would be meaningless.

garbage collection: __gc and __close

Lua provides two cleanup hooks that fire at different points in an object’s lifecycle. __gc runs when the garbage collector determines an object is unreachable and ready to be freed; it is a finalizer for releasing external resources like file handles or C allocations. __close runs deterministically when a scoped variable goes out of scope, whether the block exits normally or via an error. The two mechanisms complement each other: one is collection-driven, the other is scope-driven.

__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. The <close> annotation tells the compiler to insert an implicit call to __close at every exit point from the block, regardless of whether the block finishes normally, hits a return, or throws an error. This gives you RAII-style resource cleanup: open a file, mark it <close>, and Lua calls the file’s close method no matter how the block ends.

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 a security barrier: if you expose a table to code you don’t fully trust, preventing metatable access stops callers from swapping in their own metamethods or inspecting your internal hooks. When code calls getmetatable on a protected table, Lua returns the value of __metatable itself rather than the real metatable. Since setmetatable verifies that the current metatable is a genuine table (not a string or boolean), the guard holds: "protected" fails the check and setmetatable raises an error.

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

Studying metamethods in isolation only shows part of the picture. A real type uses several of them together: __add for arithmetic, __eq for comparisons, and __tostring for printing. Each one reinforces the others. The Fraction class below demonstrates this composition. Once you define the metamethods, all the standard operators (+, *, ==, print) work on Fraction values without any special syntax, because Lua routes every operation through the metatable automatically.

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 surprise in Lua. A Lua metamethod for garbage collection only works on userdata or C API tables. Use those if you need guaranteed finalizers.

next steps

Now that you’ve seen every Lua metamethod that Lua 5.4 provides, the natural next step is applying them to build full class systems. /tutorials/lua-oop/classes-with-metatables/ shows how to combine __index for method inheritance, __call for constructors, and __tostring for printable objects into reusable OOP patterns. If you need inheritance specifically, /tutorials/lua-oop/inheritance-in-lua/ covers single and multiple inheritance using metatable chains.

see also