Part of Learn Lua with NeoVim

Learn Lua in Neovim: Multiple Returns and Variadics — return a, b and ... | Episode 13

Sandy LaneSandy Lane

Video: Learn Lua in Neovim: Multiple Returns and Variadics — return a, b and ... | Episode 13 by Taught by Celeste AI - AI Coding Coach

Take the quiz on the full lesson page
Test what you've read · interactive walkthrough

Lua Multiple Returns and Variadics: a, b and ...

return a, b to return two values. local x, y = f() to receive both. function f(...) for variable-arity. select("#", ...) for the count, {...} to convert into a table.

Lua functions can return more than one value, and accept any number of arguments. Both features feel unusual coming from C-like languages — and both make a lot of stdlib functions cleaner.

Returning multiple values

function min_max(a, b, c)
  local mn = math.min(a, b, c)
  local mx = math.max(a, b, c)
  return mn, mx
end

local smallest, largest = min_max(5, 2, 8)
print("Min: " .. smallest .. ", Max: " .. largest)

Output:

Min: 2, Max: 8

return mn, mx returns two values. The caller uses local smallest, largest = ... to capture both.

This isn't a tuple; it's two distinct values that the language tracks separately. You don't need to construct anything to "pack" them.

A common case: division and remainder

function divide(a, b)
  return a // b, a % b
end

local q, r = divide(17, 5)
print("17 / 5 = " .. q .. " remainder " .. r)

Output:

17 / 5 = 3 remainder 2

Quotient and remainder in one call. Many Lua stdlib functions return multiple values (string.find, io.read, pcall).

Discarding with _

local _, remainder = divide(100, 7)
print("Remainder of 100/7: " .. remainder)

_ is a conventional throwaway name — assign the unwanted value to it and ignore. Lua doesn't treat _ specially; it's just a normal local variable that you (and the reader) agree not to use.

Multiple returns in the middle disappear

This is the tricky part:

function pair() return 1, 2 end

local a, b, c = pair(), 99
-- a = 1, b = 99, c = nil

When a multi-return call appears anywhere except the last position, only its first value is used. The 2 from pair() gets thrown away because there's another expression after it.

When it's last, all values are collected:

local a, b, c = 99, pair()
-- a = 99, b = 1, c = 2

This rule applies to function calls and ... (next section). It catches everyone the first time.

Using multi-return in print

print(divide(17, 5))   -- prints: 3   2 (tab-separated)
print(divide(17, 5), 99)   -- prints: 3   99 (only first value of divide)

print accepts variadic args. When divide(...) is the last argument, both its values are passed; otherwise only the first.

Variadic functions: ...

function sum(...)
  local total = 0
  local count = select("#", ...)
  local args = {...}
  for i = 1, count do
    total = total + args[i]
  end
  return total, count
end

local result, n = sum(1, 2, 3, 4, 5)
print("Sum of " .. n .. " numbers: " .. result)

local result2, n2 = sum(10, 20)
print("Sum of " .. n2 .. " numbers: " .. result2)

Output:

Sum of 5 numbers: 15
Sum of 2 numbers: 30

function sum(...) declares a variadic — accepts any number of arguments. Inside the body, ... is a magic expression that expands to all the arguments.

Three common ways to work with ...:

  1. Pack into a table: local args = {...} collects them. args[1], args[2], etc.
  2. Count them: select("#", ...) returns the count, including nils.
  3. Forward them: call another_fn(...) to pass through.

Why select("#", ...) and not #args?

local args = {1, 2, nil, 4}
print(#args)             -- 4 (sometimes 2 — undefined!)
print(select("#", ...))  -- always correct count of args passed in

Lua's # operator on a table with embedded nils is undefined — it might return any boundary. select("#", ...) reports the actual number of arguments, including nil slots.

For all-non-nil arguments, #args works. For safety with possibly-nil arguments, use select.

Mixing fixed and variadic params

function log(level, ...)
  local msg = string.format(...)
  print("[" .. level .. "] " .. msg)
end

log("INFO", "User %s logged in at %d", "Alice", 1234)

level is the fixed first parameter. ... captures everything else. We forward ... into string.format.

The variadic must be last. function log(..., level) is a syntax error.

select for picking specific args

function third(...)
  return select(3, ...)   -- everything from arg 3 onwards
end

print(third("a", "b", "c", "d"))  -- "c", "d"

select(n, ...) returns args n, n+1, n+2, ... — useful when you want to skip the front.

Forwarding a multi-return chain

function safe_call(fn, ...)
  return pcall(fn, ...)
end

pcall(fn, args...) calls fn with the args, returning true/false plus whatever fn returned. We pass ... straight through. This is the canonical "wrap a call with error handling" pattern (covered in episode 27).

Common stumbles

Multi-return swallowed mid-expression. f(g(), h()) — only the first value of g() is used. Position matters.

Forgetting select("#", ...) for variadic length. If args might include nil, #{...} is unreliable.

Using ... outside a variadic function. ... is only valid in the body of a function declared with (...). At the top level, it refers to the script's command-line arguments.

Receiving more or fewer values than returned. Extras are nil; not-asked-for ones are dropped silently. No error.

Returning a value when you meant to print it. Make sure the caller actually does something with the returned values.

What's next

Episode 14: scope. The local keyword, block scope (if, for create their own scope), shadowing, and why "always use local" is the strongest Lua tip.

Recap

return a, b, c for multiple returns; local x, y = f() to receive. Multi-return only fully expands in the last position. function f(...) for variadics; inside, {...} packs into a table, select("#", ...) counts (handles nils). Mix fixed and variadic with the variadic last: function log(level, ...). Forward with another_fn(...).

Next episode: scope.

Ready? Take the quiz on the full lesson page →
Test what you've learned. Watch the lesson and try the interactive quiz on the same page.