luaguides

Lua Closures: Upvalues and Practical Patterns

Closures and upvalues are central to idiomatic Lua programming. This guide covers Lua closures in depth: how a closure captures upvalues as live references—not copies—which enables patterns that would otherwise require verbose boilerplate. You will learn what closures actually are, how upvalues make them tick, and where both 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—each closure holds a live reference to the original variable’s memory slot, meaning that if any closure modified a, every other closure sharing that same upvalue would see the change immediately. This live-reference property underpins every pattern in the rest of this guide.

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. This design choice simplifies a huge amount of code that would otherwise need explicit capture syntax. In Python you need nonlocal x to reassign a variable from an enclosing scope. In older JavaScript with var, the scoping rules around closures are fundamentally different because var is function-scoped rather than block-scoped, leading to the infamous loop-closure bug that JavaScript developers eventually learned to work around with IIFEs or let. Lua avoids all of this ceremony. The upvalue is always writable, always a live link, and the scoping is always lexical.

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

No declaration needed. The upvalue is mutable by default. This is not merely a syntactic convenience—it means that any pattern requiring mutable state that persists across invocations (counters, caches, accumulators, state machines) can be implemented with a single closure and a local variable, no classes or objects required.

Practical Patterns

Factory Functions

The most common use of closures is a function that manufactures other functions, each with its own private state. The factory itself holds no state; it simply creates a fresh scope for each call, and the returned function captures whatever locals were in that scope. This pattern replaces what would be a class constructor in object-oriented languages, but without any metatable ceremony or prototype chains. Every call to the factory function creates an independent closure with its own upvalue storage, so the returned functions never interfere with each other.

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, so double and triple are completely isolated from each other—mutating state inside one would have no effect on the other. This independence is what makes factory functions a reliable building block for any kind of configurable behaviour generator.

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 when you want true privacy without relying on convention (like underscore-prefixed fields). A closure-based object stores all its data in upvalues, which are invisible to any code outside the closure itself. There is no way to access s.items because items was never assigned to the returned table—it lives entirely in the closure’s captured scope.

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, because the table s only contains the three functions, not the data they operate on. This is genuine encapsulation enforced by the language’s scoping rules, not by naming conventions or access modifiers. The trade-off is that you cannot introspect the state for debugging unless you explicitly expose a method for it, which is either a feature or a limitation depending on your use case.

Memoization

Closures make caching straightforward. The cache lives as an upvalue, persisting across calls. Because the cache table is captured by the returned function and not exposed anywhere else, it is safe from accidental modification by external code. The lookup cost is a single table access per call—O(1) for most inputs—making this pattern practical for expensive computations that are called repeatedly with the same arguments. The memoized version maintains the same interface as the original function, so callers do not need to know whether caching is happening behind the scenes.

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. Each call to fastFactorial first checks the cache; if the value is already computed, it returns immediately without recursing. This transforms an O(2^n) naive recursive computation into O(n) for the first call and O(1) for every subsequent call with a cached argument. The recursive self-reference (fastFactorial calling itself inside the closure) works because the local variable fastFactorial is assigned before the recursive call actually executes—Lua resolves the variable at call time, not at definition time.

Currying

Breaking a multi-argument function into a chain of single-argument closures. Currying is less common in idiomatic Lua than in functional languages like Haskell, but the pattern is occasionally useful when you want to pre-configure a function with some arguments and pass the partially applied version to a higher-order function like table.sort or a callback API.

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 inside a for loop shares the same upvalue for the loop variable. The loop variable is not re-created on each iteration—it is a single variable whose value is updated as the loop progresses. By the time any of the closures actually execute (typically after the loop has finished), the loop variable holds its final value. This is not a bug in Lua; it follows directly from the semantics of for loops and upvalue capture. Understanding why this happens is the key to avoiding it.

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 terminates, i holds the value 4 (the loop exits when i exceeds 3, leaving it at 4). Every closure sees this same final value because they all point to the same memory slot. The fix is to capture a new local per iteration, which creates a separate upvalue slot for each closure:

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. The key insight is that local j = i creates a fresh variable on each iteration of the loop, and each closure captures that iteration-specific variable rather than the shared loop counter. This pattern appears frequently in real code whenever you register a batch of callbacks inside a loop—event handlers in GUI frameworks, signal connections in game engines, or route handlers in web servers. The same principle applies when iterating with pairs, where both the key and value variables are shared across iterations and need the same local capture if closures inside the loop reference them:

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. This is a specific form of currying, but where currying typically decomposes a function into a chain of single-argument calls, partial application lets you bind any subset of arguments in any position. In Lua, the implementation is straightforward: write a function that accepts the fixed arguments, returns a closure that captures them, and then calls the underlying function when the remaining arguments arrive. The result behaves like the original function but with some of its inputs already filled in.

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