Appearance
Advanced Functions
Written: 2026-10-14
Learning objectives
- You build stateful closures and counters.
- You pass callbacks and anonymous functions.
- You write variadics and forward arguments.
- You return and consume multiple values.
- You iterate tables without pairs or ipairs.
- You guard calls with pcall.
Prerequisites
Closures hold private state
A closure captures locals from its maker. Those locals survive after the maker returns. Each maker call creates independent state. You use closures for counters, id generators, and throttles. Core mechanics live in functions.
Run this pure snippet in the Command Bar.
luau
--!strict
local function makeCounter(start: number): () -> number
local count = start
return function()
count += 1
return count
end
end
local nextA = makeCounter(0)
print(nextA())
print(nextA())
local nextB = makeCounter(100)
print(nextB())txt
1
2
101The maker creates count inside. The returned function bumps and returns it. Calls to nextA walk 1, 2. The nextB counter starts from its own 100 and prints 101. No global holds the state. Each closure owns its copy.
One-shot work rarely needs a closure. A plain function with parameters reads better. Closures fit repeated calls that must remember prior calls.
Anonymous functions go where they run
An anonymous function has no name. Define it at the use site. Pass it as a callback or store it in a table field. It always has local scope. It reads well for short handlers and comparators.
Run this pure snippet in the Command Bar.
luau
--!strict
local nums = {5, 2, 9}
table.sort(nums, function(a, b)
return a < b
end)
print(table.concat(nums, ","))txt
2,5,9The comparator lives inside the sort call. It compares two numbers with <. The table sorts ascending in place. You name the function only when you reuse it or test it alone.
Callbacks let callers inject behavior
A callback is a function you hand to another function. The receiver calls it later, often with fresh arguments. Events, sorters, and retry helpers all take callbacks. You keep callbacks short and total.
Put this Script in ServerScriptService.
luau
--!strict
local function repeatThree(action: (number) -> ())
for i = 1, 3 do
action(i)
end
end
repeatThree(function(n)
print(n * 10)
end)txt
10
20
30repeatThree accepts any function that takes a number. The call passes an anonymous print wrapper. The helper calls it with 1, 2, 3. The output scales each input by 10. You reuse the helper with different callbacks for sound, effects, or logging.
Keep callback contracts explicit. Name the parameters. Document the call count. Never assume the receiver calls you once, in order, on the same frame. The event model in functions shows callbacks firing on engine events with the same uncertainty.
Variadics take any argument count
A variadic function ends its parameter list with .... That token must come last. Inside, you pack extras with {...} or forward them with f(...). You cannot use the outer ... inside a nested function, even when nested is also variadic. Copy the values to a local table first when nesting is required.
Run this pure snippet in the Command Bar.
luau
--!strict
local function sumAll(...: number): number
local args = table.pack(...)
local total = 0
for i = 1, args.n do
total += args[i]
end
return total
end
print(sumAll(2, 4, 6))
print(sumAll(10))txt
12
10Extras pack into args. table.pack records the count in args.n, which matters because trailing nils would otherwise vanish. You loop 1 through n and accumulate. Both calls work with different arities. This pack pattern is the safe default for variadics.
Forwarding passes received extras straight into another function. Wrap behavior around the inner call without listing every parameter.
Run this pure snippet in the Command Bar.
luau
--!strict
local function logged(tag: string, fn: (...any) -> (), ...: any)
print(tag)
fn(...)
print(tag)
end
local function greet(name: string, day: string)
print(`Hi {name} on {day}`)
end
logged("wrap", greet, "Mara", "Monday")txt
wrap
Hi Mara on Monday
wraplogged prints the tag, forwards all extras to fn, then prints the tag again. The greet call receives both strings positionally. Wrappers suit timing, logging, and permission checks. The syntax tour notes the last-only rule for ....
Multiple returns move tuples
Luau functions return any number of values. List them after return separated by commas. Callers capture them in a matching variable list. Extra values drop. Missing values fill with nil. Parentheses collapse multiples to one, except in a bare return or assignment tail.
Run this pure snippet in the Command Bar.
luau
--!strict
local function splitName(full: string): (string, string)
local cut = string.find(full, " ")
if cut == nil then
return full, ""
end
return string.sub(full, 1, cut - 1), string.sub(full, cut + 1)
end
local first, last = splitName("Ada Quill")
print(first)
print(last)txt
Ada
QuillFind the space and return two slices. The caller binds both at once. Order returns from most to least important, since single-value callers take only the first piece.
table.unpack turns an array into multiple returns. It pairs naturally with table.pack for storing and replaying argument lists.
Run this pure snippet in the Command Bar.
luau
--!strict
local stored = table.pack(3, 7, 9)
local a, b, c = table.unpack(stored, 1, stored.n)
print(a + b + c)txt
19Pack stores three numbers. Unpack spreads them into three locals. The sum prints 19. This pair suits queues, logs, and deferred calls.
Generic iteration needs no pairs
Iterate a table with for k, v in t. No pairs or ipairs call is required. Luau walks array entries 1 through length first, then the rest in unspecified order. The syntax tour defines this generalized form.
Run this pure snippet in the Command Bar.
luau
--!strict
local inv = {"sword", "bow"}
for index, item in inv do
print(index, item)
end
local user = {name = "Mara", level = 4}
for key, value in user do
print(key, value)
endtxt
1 sword
2 bow
name Mara
level 4Array order is stable for dense arrays. Dictionary order is unspecified. Never rely on dictionary order for gameplay logic. Sort keys first when display order matters.
Do not add or remove keys for other entries while iterating, except by clearing the current key. That pattern keeps behavior defined. Collect changes in a side list and apply them after the loop when the edit set is large.
Custom iteration uses the __iter metamethod. When present, Luau calls it to get the iterator for your table or userdata, as covered in Metatables and OOP. Most gameplay code never defines __iter. Mention it in reviews so custom containers do not surprise you.
pcall turns crashes into values
pcall runs a function in protected mode. It returns true plus results on success. It returns false plus an error message on failure. No script stops. Branch on the boolean.
Run this pure snippet in the Command Bar.
luau
--!strict
local function double(n: number): number
return n * 2
end
local ok, out = pcall(double, 21)
print(ok)
print(out)
local bad, err = pcall(double, "x" :: any)
print(bad)
print(err == nil)txt
true
42
false
falseThe good call returns true and 42. The bad call returns false plus a message, so the message is not nil. Log the error and continue with a fallback. Full error habits continue in Errors and Defensive Code.
Use pcall at boundaries: player input, DataStores, plugins, and required modules that may fail. Do not wrap every line. Ordinary logic errors should surface in Studio so you fix them. Reserve pcall for failure you expect and can handle.
Common mistakes
Wrong: you steal the outer ... inside a nested function.
luau
--!strict
local function outer(...: number)
local function inner(): number
local args = table.pack(...)
return args.n
end
return inner()
end
print(outer(1, 2))Studio rejects the inner ... because each variadic owns its own token. Fix it by packing once outside and closing over the table.
luau
--!strict
local function outer(...: number)
local args = table.pack(...)
local function inner(): number
return args.n
end
return inner()
end
print(outer(1, 2))txt
2Wrong: you need two returns but you wrap the call.
luau
--!strict
local function pair(): (number, number)
return 1, 2
end
local packed = {pair()}
print(#packed)Parentheses are absent here, so this actually keeps both values and prints 2. The real trap is print((pair())) or local x = (pair()), where extra parentheses trim to one value. Fix it by calling without wrapping parentheses when you want the full tuple.
Wrong: you use ipairs from habit and skip dictionaries.
luau
--!strict
local user = {name = "Mara", level = 4}
for k, v in ipairs(user) do
print(k, v)
endThis prints nothing because ipairs visits only array indices. Fix it with generalized iteration.
luau
--!strict
local user = {name = "Mara", level = 4}
for k, v in user do
print(k, v)
endtxt
name Mara
level 4Wrong: you ignore the pcall boolean.
luau
--!strict
local function risky(x: number): number
assert(x > 0, "need positive")
return x * 2
end
local _, value = pcall(risky, -1)
print(value * 2)The call fails, so value holds an error string and the multiply errors again. Fix it by branching on the status first.
luau
--!strict
local function risky(x: number): number
assert(x > 0, "need positive")
return x * 2
end
local ok, value = pcall(risky, -1)
print(if ok then value else 0)txt
0Exercises
- Build two independent counters from one maker. Show their sequences diverge.
- Write a callback helper that runs a function for each array item. Reuse it with two different callbacks.
- Write a variadic max function with
table.pack. Forward its inputs to a logger first. - Return two values from a parser. Consume them, then show what extra parentheses do.
- Iterate one array and one dictionary with generalized for. Guard one pcall around a failing call.
Solution 1
Run this pure snippet in the Command Bar.
luau
--!strict
local function makeCounter(): () -> number
local n = 0
return function()
n += 1
return n
end
end
local a = makeCounter()
local b = makeCounter()
print(a())
print(a())
print(b())txt
1
2
1Each maker call owns a fresh n. The a counter reaches 2 while b starts at 1.
Solution 2
Run this pure snippet in the Command Bar.
luau
--!strict
local function each(list: { string }, fn: (string) -> ())
for _, item in list do
fn(item)
end
end
each({"a", "b"}, function(x)
print(`got {x}`)
end)
each({"a", "b"}, function(x)
print(`len {string.len(x)}`)
end)txt
got a
got b
len 1
len 1One helper visits each item. Two callbacks produce two behaviors with no helper changes.
Solution 3
Run this pure snippet in the Command Bar.
luau
--!strict
local function maxOf(...: number): number
local args = table.pack(...)
local best = args[1]
for i = 2, args.n do
if args[i] > best then
best = args[i]
end
end
return best
end
local function loggedMax(...: number): number
local args = table.pack(...)
local parts = {}
for i = 1, args.n do
parts[i] = tostring(args[i])
end
print(`inputs {table.concat(parts, ",")}`)
return maxOf(...)
end
print(loggedMax(4, 9, 2))txt
inputs 4,9,2
9Packing counts correctly, scanning finds the best, and ... forwards from wrapper to worker.
Solution 4
Run this pure snippet in the Command Bar.
luau
--!strict
local function divmod(a: number, b: number): (number, number)
return a // b, a % b
end
local q, r = divmod(17, 5)
print(q)
print(r)
local only = (divmod(17, 5))
print(only)txt
3
2
3Two locals take both values. Extra parentheses keep only the first value, 3.
Solution 5
Run this pure snippet in the Command Bar.
luau
--!strict
for i, v in {"x", "y"} do
print(i, v)
end
for k, v in {hp = 10} do
print(k, v)
end
local function boom(): number
error("nope")
end
local ok, msg = pcall(boom)
print(ok)
print(msg ~= nil)txt
1 x
2 y
hp 10
false
trueGeneralized for visits both shapes. The guarded call returns false plus a message.
Summary
- Closures own private state. Anonymous functions fit one-use callbacks.
- Variadics pack with
{...}and forward withf(...). The token stays last-only. - Multiple returns flow as tuples. Parentheses trim them.
- Iterate tables with
for k, v in t. Remember__iterfor custom containers. - Guard expected failures with pcall and branch on its boolean.
- Next: Modules and Require.