luaguides

Closures and Upvalues in Practice

Closures are one of the most useful features in Lua, and understanding how they work at the variable level unlocks patterns that would otherwise require verbose boilerplate. This guide walks through what closures actually are, how upvalues make them tick, and where they show up in real code.

What Is a Closure?

A closure is a function that captures variables from the scope where it was defined. When you define a function inside another function, the inner function can read (and write) the outer function’s local variables even after the outer function has returned.

function outer(x)
    return function()
        return x
    end
end

local get_ten = outer(10)
print(get_ten())  -- 10
# output: 10

The x parameter of outer becomes a captured variable for the returned function. In Lua terminology, x is an upvalue — a local variable from an enclosing function that a closure has taken a live reference to.

How Upvalues Work

An upvalue is not a copy of the variable’s value. It is a direct link to the original variable slot. This distinction matters: because the link is live, any mutation you make to the upvalue inside the closure affects the original variable, and that change persists across calls.

function counter(start)
    start = start or 0
    return function()
        start = start + 1
        return start
    end
end

local inc = counter(0)
print(inc())  -- 1
print(inc())  -- 2
print(inc())  -- 3
# output: 1
# output: 2
# output: 3

The start variable lives inside counter, but the anonymous function holds a reference to it. The variable survives as long as the closure itself survives.

Multiple closures can share the same upvalue. Each one sees the same underlying variable.

function pair(a, b)
    return function(op)
        if op == "sum"   then return a + b end
        if op == "product" then return a * b end
        return nil
    end
end

local f = pair(3, 4)
print(f("sum"))      -- 7
print(f("product"))  -- 12
# output: 7
# output: 12

Both a and b are upvalues of the returned function. Neither is copied.

Closures in Other Languages

Lua’s closure semantics are close to JavaScript’s let or Python’s closures post-ES6/nonlocal, but with one important difference: Lua lets you mutate an upvalue directly without any special keyword.

In Python you need nonlocal x. In older JavaScript with var, scoping rules behave differently. Lua just works.

function lua_way()
    local n = 0
    return function()
        n = n + 1
        return n
    end
end

No declaration needed. The upvalue is mutable by default.

Practical Patterns

Factory Functions

The most common use of closures is a function that manufactures other functions, each with its own private state.

function makeMultiplier(factor)
    return function(n)
        return n * factor
    end
end

local double = makeMultiplier(2)
local triple = makeMultiplier(3)
print(double(7))   -- 14
print(triple(7))   -- 21
# output: 14
# output: 21

Every call to makeMultiplier creates a fresh factor variable. Each returned function closes over its own independent copy.

Private State

Before tables-as-objects became the standard pattern, closures were Lua’s only way to hide data. Even now, closures are useful for single-instance encapsulation.

function newStack()
    local items = {}

    return {
        push = function(v) items[#items + 1] = v end,
        pop  = function()
            if #items == 0 then return nil end
            return table.remove(items)
        end,
        size = function() return #items end,
    }
end

local s = newStack()
s.push("a")
s.push("b")
print(s.pop())  -- b
print(s.size()) -- 1
# output: b
# output: 1

The items table is an upvalue shared by all three methods. It is not accessible through s.items — that is nil.

Memoization

Closures make caching straightforward. The cache lives as an upvalue, persisting across calls.

function memoize(fn)
    local cache = {}
    return function(n)
        if cache[n] then
            return cache[n]
        end
        local result = fn(n)
        cache[n] = result
        return result
    end
end

local fastFactorial = memoize(function(n)
    if n <= 1 then return 1 end
    return n * fastFactorial(n - 1)
end)

print(fastFactorial(10))  -- 3628800
print(fastFactorial(10))  -- 3628800 (cached)
# output: 3628800
# output: 3628800

The cache table survives between invocations because it is an upvalue of the returned function.

Currying

Breaking a multi-argument function into a chain of single-argument closures.

local function add(a)
    return function(b)
        return a + b
    end
end

local add_five = add(5)
print(add_five(3))   -- 8
print(add(5)(3))     -- 8 (point-free form)
# output: 8
# output: 8

The Loop Variable Gotcha

This is the most common mistake with closures in Lua. Every closure created in a loop shares the same upvalue for the loop variable, which means they all see the final value after the loop ends.

local funcs = {}
for i = 1, 3 do
    funcs[i] = function() return i end
end
print(funcs[1]())  -- 4
print(funcs[3]())  -- 4
# output: 4
# output: 4

All three functions return 4 because i is the loop variable and they all share a reference to it. After the loop, i is 4.

The fix is to capture a new local per iteration:

local funcs = {}
for i = 1, 3 do
    local j = i
    funcs[i] = function() return j end
end
print(funcs[1]())  -- 1
print(funcs[3]())  -- 3
# output: 1
# output: 3

Now each closure captures its own j. This pattern appears frequently in real code whenever you register a batch of callbacks inside a loop.

The same principle applies when iterating with pairs:

local actions = {}
local callbacks = {
    up    = function() print("up") end,
    down  = function() print("down") end,
    left  = function() print("left") end,
}

for name, fn in pairs(callbacks) do
    local n = name
    actions[name] = function() fn(); print("registered: " .. n) end
end

actions.up()
-- output: up
-- output: registered: up

Creating the intermediate local n = name inside the loop ensures each closure gets its own binding.

Partial Application

Closures let you fix some arguments of a multi-argument function and defer the rest.

local function fold(list, init, op)
    local acc = init
    for _, v in ipairs(list) do
        acc = op(acc, v)
    end
    return acc
end

local sum = function(list) return fold(list, 0, function(a, b) return a + b end) end
local product = function(list) return fold(list, 1, function(a, b) return a * b end) end

print(sum({1, 2, 3, 4}))       -- 10
print(product({1, 2, 3, 4})) -- 24
# output: 10
# output: 24

Each returned function closes over its own op and init upvalues.

Closures vs Tables as Objects

It is worth clarifying the boundary. A closure is a function with captured state. It is not an object with methods. When you need multiple instances sharing the same interface, tables with metatables are usually the better tool.

-- Closure pattern: one-off private state
local one = newStack()
one.push("x")

-- Table + metatable pattern: many instances with shared behavior
local Stack_mt = {
    __index = {
        push = function(self, v) self[#self + 1] = v end,
        pop  = function(self) return table.remove(self) end,
    }
}

local function Stack() return setmetatable({}, Stack_mt) end
local a = Stack()
local b = Stack()
a:push("y")
print(b:pop()) -- nil (b is an independent table)
# output: nil

Use closures for single-instance encapsulation or when you need true privacy without table overhead. Use the metatable pattern when you need many objects sharing methods.

Conclusion

Closures and upvalues are core to writing idiomatic Lua. A closure captures live references to its enclosing function’s locals, not copies. That distinction enables factory functions, private state, memoization, and currying. The main pitfall is the loop variable gotcha — always introduce a new local when closing over a loop variable.

Once you internalize that closures carry their environment with them, patterns that feel awkward in other languages become natural in Lua.

See Also