Functions and Tables

Reviewed & published by Brayan K

By the end of this lesson you'll define and call functions, return several values at once, accept any number of arguments with ..., give arguments sensible defaults, pass functions around as values, capture state in closures, and solve nested problems with recursion — the toolkit behind every real Lua script.

Part of the free Lua course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

What You'll Learn

💡 Real-World Analogy

A function is a recipe card. The ingredients you hand it are the arguments; the finished dish it gives back is the return value. Write the recipe once and you can cook it any night without re-deriving it. Lua's recipes have two superpowers most kitchens lack: one card can plate up several dishes at once (multiple returns), and a recipe can keep a private pantry that remembers what you used last time (a closure). Master the recipe card and you stop repeating yourself.

1. Defining and Calling Functions

A function bundles a piece of work under a name so you can run it again whenever you like. The shape is function name(args) ... end, and you put local in front to keep it scoped — the same safe default you learned for variables. Inside, return hands a value back to whoever called it; a function with no return still runs (often for a side effect like printing) and quietly gives back nil. Read this worked example and run it.

-- A function packages up some work so you can run it again and again.
-- The shape is:  function name(arguments) ... return value end
-- Put 'local' in front so the function stays scoped (your default!).
local function greet(name)
  return "Hello, " .. name .. "!"   -- '..' joins text together
end

-- Call it by writing its name followed by (arguments):
print(greet("Ada"))     -- prints: Hello, Ada!
print(greet("Lin"))     -- prints: Hello, Lin!

-- Without 'return' a function just DOES something and hands back nil:
local function shout(word)
  print(word .. "!!!")  -- this prints as a side effect
end
shout("go")             -- prints: go!!!
local result = shout("hi")  -- prints: hi!!!
print(result)           -- prints: nil   (shout returned nothing)

-- Leaving off 'local' makes a GLOBAL function — visible everywhere,
-- which is exactly the namespace pollution you want to avoid.

2. Multiple Return Values

Here's something most languages can't do: a Lua function can return several values at once — just separate them with commas after return. You catch them by listing the same number of variables on the left: local q, r = divide(17, 5). There's one rule that trips everyone up: a function call is cut down to a single value unless it's the last thing in a list. Put the multi-return call last and all its values survive; put it earlier and only the first is kept.

-- Lua functions can return MORE THAN ONE value. Just list them after return.
local function divide(a, b)
  return a // b, a % b      -- // is integer divide, % is remainder
end

-- Catch each returned value in its own variable, left to right:
local q, r = divide(17, 5)
print(q, r)                 -- prints: 3   2

-- A handy real example: split a full name into first and last.
local function splitName(full)
  return full:match("(%w+)%s+(%w+)")   -- pattern returns TWO captures
end
local first, last = splitName("Grace Hopper")
print(first, last)          -- prints: Grace   Hopper

-- ⚠️ A function call is TRUNCATED to one value unless it's LAST in a list.
local function pair() return 1, 2 end
print(pair(), 99)           -- prints: 1   99   (pair() cut to just 1)
print(99, pair())           -- prints: 99   1   2   (pair() is last -> all kept)

Your turn. Finish the minMax function so it returns the smaller value first and the larger second, then catch both returned values. Fill in the blanks marked ___ and compare with the expected output.

-- 🎯 YOUR TURN — replace each ___ then run the code.

-- Write a function that returns BOTH the min and the max of two numbers.
local function minMax(a, b)
  -- 1) If a is smaller, the order is a, b — otherwise b, a
  if a < b then
    return ___, ___      -- 👉 smaller first, then larger: a, b
  else
    return ___, ___      -- 👉 b, a
  end
end

-- 2) Catch BOTH returned values in two variables, left to right
local low, high = ___    -- 👉 minMax(8, 3)

print(low, high)

-- ✅ Expected output:
-- 3   8

3. Varargs: Any Number of Arguments

Sometimes you don't know in advance how many arguments a function will get — think print, which accepts as many as you throw at it. Lua's answer is varargs, written as ... (three dots) in the parameter list. Pack them into a table with {...} so you can loop over them, count them with select("#", ...), or read from a position with select(i, ...). When some arguments might be nil, use table.pack(...) instead — it records an accurate count in .n that {...} can't promise.

-- '...' (three dots) means "any number of extra arguments".
-- It collects everything the caller passed into a vararg list.
local function sum(...)
  local total = 0
  for _, n in ipairs({...}) do   -- {...} packs the args into a table
    total = total + n
  end
  return total
end
print(sum(1, 2, 3))         -- prints: 6
print(sum(10, 20, 30, 40))  -- prints: 100

-- select("#", ...) counts how many varargs arrived.
-- select(i, ...)   returns everything from position i onward.
local function describe(...)
  print("got " .. select("#", ...) .. " arguments")
  print("first one is " .. select(1, ...))
end
describe("a", "b", "c")
-- prints: got 3 arguments
-- prints: first one is a

-- table.pack keeps a correct count even when some args are nil.
local packed = table.pack(1, nil, 3)
print(packed.n, packed[1], packed[3])   -- prints: 3   1   3

Now you try. Build an average function that accepts any number of scores using ..., sums them, and divides by how many there were. Fill in the three blanks:

-- 🎯 YOUR TURN — build an average() that accepts any number of scores.

-- 1) Use ... so the caller can pass as many numbers as they like
local function average(...)
  local total = 0
  local nums = {___}            -- 👉 pack the varargs into a table: ...

  for _, n in ipairs(nums) do
    total = total + n
  end

  -- 2) How many numbers came in? Use the # length operator on the table
  local count = ___             -- 👉 #nums

  -- 3) Avoid divide-by-zero: if count is 0, the answer is 0
  if count == 0 then return 0 end
  return total / count
end

print(average(10, 20, 30))      -- 👉 call it with three numbers
print(average(4, 8))

-- ✅ Expected output:
-- 20.0
-- 6.0

4. Default Argument Values

Lua has no special syntax for default arguments, but there's a one-line idiom every Lua programmer uses: x = x or fallback. The or operator returns its first truthy value, so when an argument is missing (it arrives as nil) the fallback kicks in. The single gotcha: or also treats false as "missing", so for boolean flags compare against nil directly (if flag == nil then ... end) — otherwise a deliberately passed false would be overwritten.

-- Lua has no built-in default arguments, so use the 'or' idiom:
--   local x = x or fallback
-- 'or' returns its first TRUTHY value, so a missing (nil) arg falls back.
local function greet(name, greeting)
  name = name or "stranger"        -- if name is nil, use "stranger"
  greeting = greeting or "Hello"   -- if greeting is nil, use "Hello"
  return greeting .. ", " .. name .. "!"
end

print(greet())                 -- prints: Hello, stranger!
print(greet("Ada"))            -- prints: Hello, Ada!
print(greet("Ada", "Hi"))      -- prints: Hi, Ada!

-- ⚠️ Watch out: 'or' treats false and nil the same.
-- For boolean flags, compare to nil instead so 'false' survives:
local function setup(verbose)
  if verbose == nil then verbose = true end   -- only default when truly missing
  return verbose
end
print(setup())        -- prints: true    (defaulted)
print(setup(false))   -- prints: false   (kept!)

5. Functions as Values & Closures

In Lua, functions are first-class values: you can store one in a variable, pass it into another function, return it, or drop it in a table — exactly like a number or a string. This unlocks the closure: a function that remembers the local variables alive where it was created. Those captured variables stay private and persist between calls, which is how you build counters and accumulators without reaching for globals. Crucially, each closure gets its own copy of the captured state.

-- Functions are FIRST-CLASS VALUES: store them, pass them, return them.
local function square(x) return x * x end
local f = square              -- store a function in a variable
print(f(5))                   -- prints: 25

-- Pass a function as an argument (here, apply it to every item):
local function applyAll(fn, list)
  local out = {}
  for i, v in ipairs(list) do out[i] = fn(v) end
  return out
end
local doubled = applyAll(function(n) return n * 2 end, {1, 2, 3})
print(doubled[1], doubled[2], doubled[3])   -- prints: 2   4   6

-- A CLOSURE is a function that remembers variables from where it was made.
-- 'count' lives on between calls because the inner function captured it.
local function makeCounter()
  local count = 0
  return function()
    count = count + 1     -- updates the captured 'count'
    return count
  end
end
local next = makeCounter()
print(next(), next(), next())   -- prints: 1   2   3

-- Each closure gets its OWN private copy — they don't share state:
local a, b = makeCounter(), makeCounter()
print(a(), a(), b())            -- prints: 1   2   1

6. Recursion

Recursion is a function that calls itself, solving a big problem by reducing it to a smaller version of the same problem. Every recursive function needs two parts: a base case that stops the chain, and a recursive step that moves toward that base case by shrinking the input. Forget the base case and the calls never stop — you'll get a stack overflow. Recursion is the natural fit for anything nested: folder trees, JSON, or maths like factorial defined in terms of itself.

-- RECURSION: a function that calls itself, shrinking the problem each time.
-- Every recursion needs a BASE CASE so it eventually stops.
local function factorial(n)
  if n <= 1 then return 1 end        -- base case: stop here
  return n * factorial(n - 1)        -- recursive step: gets smaller
end
print(factorial(5))    -- prints: 120   (5*4*3*2*1)

-- Recursion shines on nested data, like counting files in a folder tree.
local function countdown(n)
  if n == 0 then
    print("liftoff!")
    return
  end
  print(n)
  countdown(n - 1)     -- call self with a smaller number
end
countdown(3)
-- prints: 3
-- prints: 2
-- prints: 1
-- prints: liftoff!

Pro Tips

Common Errors (and the fix)

📋 Quick Reference

TaskCodeResult / Note
Local functionlocal function f(x) endscoped to block
Anonymous functionlocal f = function(x) endstored in a variable
Multiple returnreturn a, blocal x, y = f()
Varargsfunction f(...) end{...} packs them
Count varargsselect("#", ...)how many passed
Default argumentx = x or 0fallback when nil
Force one value(f())truncates to 1

Mini-Challenge: Bank Account Closure

No blanks this time — just a brief and an outline to keep you on track. You'll combine three of this lesson's ideas: the default-argument idiom, returning multiple functions, and closures that share captured state. Build it, run it, and check your output against the example in the comments.

-- 🎯 MINI-CHALLENGE: a bank-account closure
-- 1. Write makeAccount(balance) that takes a starting balance.
--    Use the default idiom so a missing balance starts at 0:
--       balance = balance or 0
-- 2. Inside, return TWO closures that both capture 'balance':
--       deposit(amount)  -> adds to balance, returns the new balance
--       withdraw(amount) -> subtracts, returns the new balance
--    (return them as:  return deposit, withdraw)
-- 3. Create an account, then deposit and withdraw and print the results.
--
-- ✅ Example run:
--   local deposit, withdraw = makeAccount(100)
--   print(deposit(50))    -- 150
--   print(withdraw(30))   -- 120
--   print(deposit(10))    -- 130   (balance persisted between calls!)

-- your code here

🎉 Lesson Complete!

Practice quiz

Which keyword starts the return of a value from a Lua function?

  • yield
  • return
  • give
  • out

Answer: return. return hands a value back to the caller.

How do you make a function scoped to its block?

  • scoped function
  • private function
  • local function
  • static function

Answer: local function. local function keeps the function out of the global namespace.

How do you capture both values of return a, b ?

  • local x = f()
  • local x[2] = f()
  • local x; x = f()
  • local x, y = f()

Answer: local x, y = f(). List two variables on the left to receive both returns.

A multi-return call keeps all its values only when it is...

  • The last item in a list
  • The first item in a list
  • Wrapped in parentheses
  • Assigned to one variable

Answer: The last item in a list. Calls are truncated to one value unless they are last in a list.

What does ... mean in a parameter list?

  • A range
  • Varargs: any number of extra arguments
  • A default value
  • A comment

Answer: Varargs: any number of extra arguments. ... collects any number of extra arguments passed by the caller.

Which idiom gives a default for a missing argument?

  • x = default(x)
  • x = x and default
  • x = x or default
  • x ??= default

Answer: x = x or default. or returns its first truthy value, so a nil argument falls back to the default.

What is a closure?

  • A loop that closes itself
  • A type of table
  • A way to end a function early
  • A function that remembers variables from where it was created

Answer: A function that remembers variables from where it was created. A closure captures and keeps the local variables alive where it was made.

Every recursive function must have a...

  • Base case that stops it
  • Global counter
  • Vararg parameter
  • Return of nil

Answer: Base case that stops it. Without a base case the recursion never stops and overflows the stack.

What does select("#", ...) return?

  • The first vararg
  • The last vararg
  • How many varargs were passed
  • A packed table

Answer: How many varargs were passed. select("#", ...) counts the varargs received.

A function with no return statement gives back...

  • 0
  • An empty string
  • An error
  • nil

Answer: nil. With no return, a Lua function hands back nil.

Continue this course

Frequently asked questions

Why do I only get the first value back from a multi-return function?

A function call is trimmed to a single value whenever it is NOT the last item in a list. So print(pair(), 99) keeps only pair()'s first value, while print(99, pair()) keeps them all because the call sits last. To capture several returns, assign them: local a, b = pair(). To force just one value anywhere, wrap the call in parentheses — (pair()) is always exactly one value.

What is the difference between local function f() and function f()?

Both create a function, but 'function f()' creates a GLOBAL named f that the entire program and every library can see and overwrite. 'local function f()' keeps f scoped to the current block, which is safer and slightly faster to call. Default to local: a global function is just namespace pollution unless you specifically need it everywhere. 'local function' also lets the function refer to itself for recursion.

How do I give a function a default argument value?

Lua has no built-in defaults, so use the 'or' idiom inside the function: write 'name = name or "stranger"'. Because 'or' returns its first truthy operand, a missing (nil) argument falls back to your default. One catch: 'or' also rejects false, so for boolean flags compare to nil instead — 'if flag == nil then flag = true end' — otherwise passing false would be overwritten.

What exactly is a closure, and why is it useful?

A closure is a function bundled with the outer local variables it referenced when it was created — those variables stay alive and private after the outer function returns. That lets you build counters, accumulators, and bank-account style state without globals: each call to makeCounter() returns a fresh closure with its own private 'count'. Two closures made separately do not share state.

When should I use recursion instead of a loop?

Reach for recursion when the data is itself nested or tree-shaped — walking a folder tree, traversing JSON, or maths defined in terms of itself like factorial. Every recursion needs a BASE CASE that stops it, plus a step that makes the problem smaller each call; miss either and you get a 'stack overflow'. For simple repetition over a list, a plain for-loop is usually clearer and lighter.

What do the three dots (...) mean in a parameter list?

'...' is the vararg expression: it collects any extra arguments the caller passed. Capture them with {...} to make a table, count them with select("#", ...), or grab from a position with select(i, ...). When some arguments might be nil, use table.pack(...) instead of {...} because it stores an accurate count in the .n field, which {...} and # cannot guarantee.