Skip to content

Lua Functions Guide — First-Class Functions, Closures, and Varargs

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about Lua Functions Guide. We cover key concepts, practical examples, and best practices to help you master this topic.

Lua functions are first-class values -- they can be assigned to variables, passed as arguments to other functions, returned as results, and captured in closures with lexical scoping.

What You'll Learn

  • Defining and calling functions with multiple returns
  • Using functions as first-class values
  • Creating closures with captured variables
  • Handling variable arguments with ...

Why It Matters

Functions are the primary means of abstraction in Lua. Without built-in classes, functions and closures are how you organize code, create callbacks, implement iterators, and build reusable abstractions. Durga Antivirus Pro uses Lua closures for threat detection rules that capture context from their environment.

Real-World Use

Love2D games use functions for draw and update callbacks. Redis Lua scripts use functions for atomic operations on the server. Nginx/OpenResty uses Lua closures for request handlers that capture configuration variables.

flowchart LR
    A["Functions"] --> B["Definition"]
    A --> C["Returns"]
    A --> D["Closures"]
    A --> E["Varargs"]
    B --> F["Callbacks"]
    D --> F
    A:::current --> B
    style A fill:#2563eb,stroke:#2563eb,color:#fff
    style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b

Defining Functions

-- Standard syntax
local function greet(name)
    return "Hello, " .. name
end

-- Anonymous function assigned to variable
local greet = function(name)
    return "Hello, " .. name
end

-- Calling
print(greet("Lua"))  --> Hello, Lua

Syntax Details

The end keyword terminates every function. The return keyword is optional -- if omitted, the function returns nil.

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

local function log_message(msg)
    print("[LOG] " .. msg)
    -- implicit return nil
end

Multiple Return Values

Lua functions can return multiple values -- a feature that eliminates the need for tuple or wrapper objects.

local function divide(a, b)
    if b == 0 then
        return nil, "division by zero"
    end
    return a / b, nil
end

-- Capture all returns
local result, err = divide(10, 3)
print(result, err)  --> 3.3333333333333    nil

local result2, err2 = divide(10, 0)
print(result2, err2)  --> nil    division by zero

-- Discard values with _
local ok, _ = divide(10, 2)
print(ok)  --> 5

Common Built-in Multiple Returns

-- string.find
local start, finish = string.find("hello world", "world")
print(start, finish)  --> 7    11

-- ipairs iterator
local iter, tbl, index = ipairs({10, 20, 30})
print(iter, type(tbl), index)  --> function    table   0

Functions as First-Class Values

-- Store in table
local operations = {
    add = function(a, b) return a + b end,
    sub = function(a, b) return a - b end,
    mul = function(a, b) return a * b end
}
print(operations.add(5, 3))  --> 8

-- Pass as argument (higher-order function)
local function apply(func, value)
    return func(value)
end

local double = function(x) return x * 2 end
print(apply(double, 21))  --> 42

-- Return function from function
local function make_counter()
    local count = 0
    return function()
        count = count + 1
        return count
    end
end

local counter = make_counter()
print(counter())  --> 1
print(counter())  --> 2
print(counter())  --> 3

Closures

A closure is a function plus all the local variables it captures from its enclosing scope (called "upvalues").

local function make_adder(n)
    -- n is captured by the returned function
    return function(x)
        return x + n
    end
end

local add5 = make_adder(5)
local add10 = make_adder(10)

print(add5(20))   --> 25
print(add10(20))  --> 30

Practical Closure: Iterator with State

local function range_iterator(start, stop)
    local i = start - 1
    return function()
        i = i + 1
        if i <= stop then
            return i
        end
        return nil
    end
end

for i in range_iterator(1, 5) do
    print(i)
end

Expected output:

1
2
3
4
5

Variable Arguments (Varargs)

Functions can accept any number of arguments using ....

local function sum(...)
    local total = 0
    for i = 1, select("#", ...) do
        total = total + select(i, ...)
    end
    return total
end

print(sum(1, 2, 3))          --> 6
print(sum(10, 20, 30, 40))   --> 100

-- Using select for individual args
local function first_and_last(...)
    local first = (...)
    local last = select(select("#", ...), ...)
    return first, last
end

local f, l = first_and_last(1, 2, 3, 4, 5)
print(f, l)  --> 1    5

Varargs with Pack

In Lua 5.2+, use table.pack and table.unpack for flexible vararg handling.

local function process(...)
    local args = table.pack(...)
    print("Received " .. args.n .. " arguments")
    for i = 1, args.n do
        print("  arg " .. i .. ": " .. tostring(args[i]))
    end
end

process("a", "b", "c")

Expected output:

Received 3 arguments
  arg 1: a
  arg 2: b
  arg 3: c

Method Syntax (Colon Operator)

Lua's colon syntax is syntactic sugar for method calls -- it passes self implicitly.

local person = {
    name = "Alice",
    greet = function(self, greeting)
        return greeting .. ", I'm " .. self.name
    end
}

-- Without colon (explicit self)
print(person.greet(person, "Hi"))  --> Hi, I'm Alice

-- With colon (implicit self)
print(person:greet("Hi"))          --> Hi, I'm Alice

Common Mistakes

1. Forgetting return

Without return, a function returns nil. local result = my_func() silently gets nil.

2. Confusing parameters with arguments

Parameters are the variables in the function definition. Arguments are the values passed when calling.

3. Overlooking that = returns nil

A function with no explicit return returns nil. This surprises developers from languages with implicit returns.

4. Creating closures in loops incorrectly

-- Wrong: all closures share the same i
local funcs = {}
for i = 1, 3 do
    funcs[i] = function() print(i) end
end

In Lua 5.3+, this works correctly. In Lua 5.2 and earlier, capture a copy: local j = i.

5. Mixing tab and space indentation

Lua doesn't care about indentation, but readable code matters. Be consistent.

Practice Questions

1. What makes Lua functions "first-class"? Functions are values. They can be stored in variables, passed as arguments, returned from other functions, and stored in tables.

2. What is a closure in Lua? A function that captures local variables from its enclosing scope. The captured variables (upvalues) persist as long as the closure exists.

3. How do you return multiple values from a Lua function? Separate them with commas after return: return a, b, c. The caller captures them with multiple assignment: local x, y, z = my_func().

4. What does ... mean in a function definition? It represents variable arguments (varargs). The function can receive any number of arguments, accessed through ... and select().

Challenge: Write a function that takes variable number of numbers, returns the minimum, maximum, and average as three separate values.

FAQ

{{< faq question="Can I define default parameter values?" >}} Lua doesn't have default parameters built in. Use name = name or "default" inside the function to simulate them. Be careful with false values -- use a stricter check: if name == nil then name = "default" end. {{< /faq >}}

{{< faq question="What is tail call optimization?" >}} Lua supports proper tail calls -- if a function ends with return func(args), the current function's stack frame is reused. This enables infinite Recursion without stack overflow for tail-recursive functions. {{< /faq >}}

{{< faq question="Can I call a function stored in a table?" >}} Yes. Use table.func(args) for functions stored as values, or table:method(args) for methods that need the self parameter. {{< /faq >}}

{{< faq question="How do I create an optional parameter?" >}} Check if the parameter is nil: function greet(name) name = name or "World" ... end. Or use select to count arguments. {{< /faq >}}

{{< faq question="What is the difference between local function and function?" >}} local function is scoped to the enclosing block (function-body or file). function foo() assigns to a global. Always use local function unless you specifically need globals. {{< /faq >}}

Try It Yourself

local function calculator(a, b, op)
    local operations = {
        add = function(x, y) return x + y end,
        sub = function(x, y) return x - y end,
        mul = function(x, y) return x * y end,
        div = function(x, y)
            if y == 0 then return nil, "division by zero" end
            return x / y
        end
    }
    local func = operations[op]
    if not func then
        return nil, "unknown operation: " .. op
    end
    return func(a, b)
end

-- Test the calculator
local r1, _ = calculator(10, 5, "add")
print("10 + 5 = " .. r1)

local r2, _ = calculator(10, 5, "mul")
print("10 * 5 = " .. r2)

local r3, err = calculator(10, 0, "div")
if err then
    print("Error: " .. err)
end

Expected output:

10 + 5 = 15
10 * 5 = 50
Error: division by zero

What's Next

Now that you understand functions, learn about metatables -- Lua's mechanism for operator overloading and prototypal inheritance.

Topic Description Link
Metatables Operator overloading and metamethods {{< ref "06-metatables" >}}
OOP Object-oriented programming with prototypes {{< ref "07-oop-prototypes" >}}
JavaScript Functions Compare with JavaScript closures JavaScript

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro