Variadic Functions and Multiple Returns
Two features set Lua's function calling convention apart from many mainstream languages: a single return statement can hand back several values at once, and a function parameter list can end with ... to accept any number of extra arguments. Together these make Lua's standard library functions, like string.find or table.remove, flexible without needing wrapper objects or fixed-size argument arrays.
Cricket analogy: A player run out attempting a risky second run illustrates a single outcome, but Lua functions are more like a post-match report that can hand back several results at once, runs scored, balls faced, and strike rate, all in one return statement.
Returning Multiple Values
How many of a call's return values are actually kept depends entirely on context. When a multi-value call is the last (or only) item in a list — the right-hand side of an assignment, the last argument in a function call, or the last element of a table constructor — all of its values are used. In any other position, or when wrapped in extra parentheses like (f()), it is truncated to exactly one value.
Cricket analogy: local runs, balls, fours = getInnings() grabs all three stats cleanly, but if you wrote (getInnings()) in parentheses, Lua truncates it to just the first value, runs, the way a headline stat crops out the fuller box score.
-- returning multiple values
local function minMax(t)
local lo, hi = t[1], t[1]
for _, v in ipairs(t) do
if v < lo then lo = v end
if v > hi then hi = v end
end
return lo, hi
end
local lo, hi = minMax({4, 9, 1, 7})
print(lo, hi) --> 1 9
-- multi-return truncation with parentheses
print(minMax({4, 9, 1, 7})) --> 1 9 (both values, last position)
print((minMax({4, 9, 1, 7}))) --> 1 (parens truncate to one value)
-- variadic function
local function sum(...)
local total = 0
local n = select('#', ...)
for i = 1, n do
total = total + select(i, ...)
end
return total
end
print(sum(1, 2, 3, 4)) --> 10
-- table.pack / table.unpack
local function packArgs(...)
local packed = table.pack(...)
print("count:", packed.n)
return packed
end
local packed = packArgs(10, nil, 30)
print(table.unpack(packed, 1, packed.n)) --> 10 nil 30Variadic Parameters with ...
A function whose last parameter is ... accepts any number of extra arguments. Inside the body, {...} collects them into a table, select('#', ...) returns the exact count of arguments passed (correctly handling nil values in the middle), and select(n, ...) returns every argument from position n onward, which is useful for slicing off the front of a variadic argument list.
Cricket analogy: function totalRuns(...) end lets a scorer tally an unknown number of partnership contributions in one call, like totalRuns(45, 30, 12, 67), and select('#', ...) tells you exactly how many partnerships were passed in, even if some contributed zero runs.
Always prefer select('#', ...) over #{...} to count variadic arguments. Building a table with {...} and taking its length with the # operator is unreliable when the argument list contains nil holes, because # on a table with holes has undefined behavior — select('#', ...) always returns the exact original argument count.
table.pack and table.unpack
table.pack(...) bundles a variadic argument list into a plain table plus an .n field recording exactly how many arguments were packed (including trailing nils), which is useful for storing or passing around a variable-length argument list as a single value. table.unpack(t, i, j) does the reverse, spreading a table's elements back out into a multi-value expression, typically used to forward stored arguments into another call.
Cricket analogy: table.pack(...) bundles an unknown number of over-by-over scores into a table with a .n field recording exactly how many overs were bowled, and table.unpack(t) later spreads them back out for a full innings recap.
Common Pitfalls with Multiple Returns
Only the last expression in a list expands to all of its values; every earlier multi-return expression in that same list is truncated to just its first value. This trips up code that chains multi-return helper functions as arguments, since only the final argument in a call gets the full benefit of multiple returns — everything before it silently loses extra values.
Cricket analogy: Only the last function call in a list of returns expands fully, return getScore(), getWickets() gives just one score value plus all wicket values, the way only the final batter's dismissal gets the full commentary detail while earlier ones are summarized in one line.
Multi-value expansion only happens for the LAST expression in a list — arguments to a call, elements of a table constructor, or values in a return/assignment. Every earlier multi-return expression in that list is truncated to exactly one value. Wrapping any multi-return call in extra parentheses, like (f()), also always truncates it to one value, even in the last position.
- A Lua function can return multiple values with a single return a, b, c statement.
- Multiple return values are only fully kept when the call is the last (or only) expression in a list; elsewhere they're truncated to one value.
- Wrapping a call in extra parentheses, (f()), forces it to exactly one value regardless of position.
- ... inside a function collects variadic arguments; select('#', ...) gives the exact count (safe with nil holes), and select(n, ...) returns all arguments from position n onward.
- table.pack(...) bundles varargs into a table with an .n field for the count; table.unpack(t) spreads a table's values back into a multi-value expression.
- Use select('#', ...) rather than #{...} to count varargs reliably, since the # operator's behavior on tables with nil holes is undefined.
Practice what you learned
1. What does `print((f()))` do if f() returns 3 values?
2. How do you reliably count the number of arguments passed to a variadic function, including nil arguments?
3. In `return f(), g()`, which call's return values are fully expanded?
4. What does `table.pack(1, 2, 3)` return?
5. What does `select(2, 'a', 'b', 'c')` return?
Was this page helpful?
You May Also Like
Functions in Lua
Learn how to define and call Lua functions, treat them as first-class values, use colon syntax for methods, and understand local vs global function scope.
Closures in Lua
Understand how Lua closures capture upvalues by reference, how to use them for private state and per-iteration loop callbacks, and common closure-based patterns like memoization.
Loops in Lua
A practical guide to Lua's while, repeat...until, numeric for, and generic for loops, plus how break and Lua's lack of a continue keyword affect loop control.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics