Functions, Closures, and Varargs
Functions are the building blocks of any Lua program. They let you encapsulate logic, avoid repetition, and structure your code into manageable pieces. Lua treats functions as first-class values — you can assign them to variables, pass them as arguments, and return them from other functions. This design makes Lua functions and closures feel natural rather than bolted on. In this tutorial, you’ll learn how to define functions, capture state with closures, work with variable arguments, and apply best practices for clean, maintainable Lua code.
Defining Functions
Lua provides several ways to define functions. The most common syntax uses the function keyword:
function greet(name)
print("Hello, " .. name .. "!")
end
greet("World") -- Output: Hello, World!
Lua’s function name() syntax is syntactic sugar for name = function(). Under the hood, both forms create an anonymous function value and bind it to a name; the only difference is that local function add() is shorthand for local add; add = function(). When you omit local, the function lands in the global table _G and becomes visible everywhere, which risks accidental overwrites by other scripts or libraries. For any function that does not need global visibility, assigning it to a local variable keeps the global namespace clean and gives a small performance benefit because local variable lookup is faster than indexing into _G.
local add = function(a, b)
return a + b
end
print(add(3, 5)) -- Output: 8
Both approaches create a function, but the first declares it globally while the second assigns it to a local variable. Prefer local functions—they’re cleaner and avoid polluting the global namespace.
Multiple return values
Lua functions can return multiple values, which is particularly useful:
function divide(a, b)
if b == 0 then
return nil, "division by zero"
end
return a / b, nil
end
local result, err = divide(10, 2)
print(result) -- Output: 5
print(err) -- Output: nil
This pattern lets you return both a value and an error indicator, similar to Go’s error handling style. Be aware that Lua silently discards extra return values when you capture fewer variables than the function returns—writing local result = divide(10, 2) drops the error message entirely, which can mask bugs. Multiple return values also behave differently depending on their position in an expression list: only the last expression expands to multiple values, while earlier positions are truncated to one. Functions that return variable numbers of values can surprise callers who assume a fixed return count.
Closures
A closure is a function that captures variables from its surrounding scope. In Lua, closures bind to the actual upvalues, not copies, so each invocation of the outer function creates an independent set of captured variables. This powerful feature enables interesting patterns:
function counter()
local count = 0
return function()
count = count + 1
return count
end
end
local c1 = counter()
local c2 = counter()
print(c1()) -- Output: 1
print(c1()) -- Output: 2
print(c2()) -- Output: 1
Each call to counter() creates a new closure with its own count variable. The returned function “remembers” the environment where it was created.
Closures are useful for:
- Creating private variables
- Building stateful functions
- Implementing callbacks and event handlers
Variable arguments (varargs)
Lua supports variable arguments using the ... syntax:
function sum(...)
local total = 0
for _, v in ipairs({...}) do
total = total + v
end
return total
end
print(sum(1, 2, 3)) -- Output: 6
print(sum(10, 20, 30, 40)) -- Output: 100
Packing ... into a table with {...} is convenient, but it allocates a new table on every call and silently drops trailing nil arguments because nil terminates table construction. For functions that receive hundreds of arguments or need to preserve explicit nil values, select() offers a lighter alternative. The select("#", ...) form returns the exact argument count, while select(i, ...) retrieves the i-th argument and all that follow without creating any intermediate table. This matters in hot loops where avoiding table allocation measurably improves performance.
function average(...)
local count = select("#", ...) -- number of arguments
local total = 0
for i = 1, count do
total = total + select(i, ...)
end
return total / count
end
print(average(10, 20, 30)) -- Output: 20
Multiple assignments with varargs
A vararg function can forward its arguments to another function by using ... directly in the call. Lua expands ... into individual values at the call site, making delegation straightforward. Capturing multiple return values into separate local variables follows the same expansion rules, if you write local a, b = someFunc(), Lua assigns the first return to a, the second to b, and silently discards any extras. This destructuring-like syntax works with any function, not just vararg ones, and pairs naturally with the multiple-return error-handling pattern shown earlier.
function stats(numbers)
local sum = 0
for _, n in ipairs(numbers) do
sum = sum + n
end
local mean = sum / #numbers
return sum, mean, #numbers
end
local total, average, count = stats({10, 20, 30})
print(total, average, count) -- Output: 60 20 3
Named Returns
Positional returns work well for two or three values, but once a function produces four or more outputs, callers struggle to remember which position holds each datum. Table-based returns solve this by labeling every value, turning local t, a, c = stats(data) into local r = stats(data) followed by r.total, r.average, and r.count. The trade-off is a single table allocation per call, but the clarity gain almost always outweighs the cost. Lua’s table constructor also lets you embed computed fields inline, so the return expression itself can be a single return {...} with the values computed right inside the braces.
function rectangle(w, h)
return {
width = w,
height = h,
area = w * h,
perimeter = 2 * (w + h)
}
end
local r = rectangle(5, 3)
print(r.area) -- Output: 15
print(r.perimeter) -- Output: 16
This pattern improves readability when returning many values.
Best Practices
-
Use local functions: Avoid polluting the global namespace.
-
Keep functions focused: Each function should do one thing well.
-
Validate inputs: Check for nil values and invalid arguments:
function safeDivide(a, b)
assert(type(a) == "number", "first argument must be a number")
assert(type(b) == "number", "second argument must be a number")
assert(b ~= 0, "cannot divide by zero")
return a / b
end
-
Use multiple returns for errors: Return
nilwith an error message instead of throwing exceptions. -
Document with comments: Explain what the function does, its parameters, and return values.
Common Patterns
Lua’s first-class functions enable several reusable design patterns that show up across Lua codebases. Functions passed as arguments let you parameterize behavior without subclassing or interface boilerplate, while functions that return other functions let you configure specialized variants from a general template. Both patterns lean on closures to carry configuration state without global variables.
Callback Functions
function processItems(items, callback)
local results = {}
for i, item in ipairs(items) do
results[i] = callback(item)
end
return results
end
local doubled = processItems({1, 2, 3}, function(x) return x * 2 end)
print(table.concat(doubled, ", ")) -- Output: 2, 4, 6
The callback pattern keeps processItems generic, it knows nothing about what transformation to apply, only that it should iterate and collect results. That separation means you can reuse the same iteration logic for doubling, squaring, filtering, or any other per-element operation without touching the loop. A common Lua pitfall here involves closures inside loops: when the callback captures the loop variable i by reference, all callbacks end up seeing its final value after the loop finishes. Using local inside the loop body or passing the value as a function argument avoids this capture-by-reference trap.
Function Factories
function multiplier(factor)
return function(x)
return x * factor
end
end
local double = multiplier(2)
local triple = multiplier(3)
print(double(5)) -- Output: 10
print(triple(5)) -- Output: 15
Summary
Functions in Lua are versatile and powerful. Key takeaways:
- Define functions with
function name(args) ... end— the standard syntax works for both global and local functions - Use closures to create stateful, reusable functions that remember the environment where they were born
- Handle variable arguments with
...andselect— choose the right tool based on whether you need the full argument list or just the count - Return multiple values for clean error handling, following the same pattern Go uses: a result and an optional error
- Prefer local functions and validate inputs early — catching bad arguments at the top of a function prevents confusing errors downstream
In the next tutorial, you’ll learn about Tables: Lua’s Universal Data Structure, which combine arrays, dictionaries, and objects into one flexible type.
See also
- Variables and Types in Lua — Learn about Lua’s basic data types
- Control Flow: if, for, while, and repeat — Master conditional statements and loops
- Tables: Lua’s Universal Data Structure — Explore tables, the core data structure in Lua