100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

Variadic Functions and Multiple Returns

Understand how Lua functions return multiple values, how variadic parameters (...) work with select and table.pack/unpack, and the truncation rules that trip up newcomers.

Control Flow & FunctionsIntermediate11 min readJul 10, 2026
Analogies

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.

lua
-- 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  30

Variadic 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

Was this page helpful?

Topics covered

#Programming#LuaStudyNotes#VariadicFunctionsAndMultipleReturns#Variadic#Functions#Multiple#Returns#StudyNotes#SkillVeris#ExamPrep