---
url: /luau-basics/functions-advanced.md
description: Closures with state, callbacks, variadics, multiple returns, and pcall basics.
---

# Advanced Functions

*Written: {{ $frontmatter.date }}*

## 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

* [Functions and Scope](../programming-fundamentals/functions-and-scope.md)
* [Conditionals and Loops](../programming-fundamentals/conditionals-and-loops.md)
* [Variables and Values](../programming-fundamentals/variables-and-values.md)
* [Syntax and Control Flow](./syntax-and-control-flow.md)

## 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](https://create.roblox.com/docs/luau/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
101
```

The 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,9
```

The 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
30
```

`repeatThree` 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](https://create.roblox.com/docs/luau/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
10
```

Extras 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
wrap
```

`logged` 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](https://luau.org/syntax) 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
Quill
```

Find 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
19
```

Pack 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](https://luau.org/syntax) 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)
end
```

```txt
1	sword
2	bow
name	Mara
level	4
```

Array 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](./metatables-and-oop.md). 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
false
```

The 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](./errors-and-defensive-code.md).

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
2
```

Wrong: 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)
end
```

This 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)
end
```

```txt
name	Mara
level	4
```

Wrong: 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
0
```

## Exercises

1. Build two independent counters from one maker. Show their sequences diverge.
2. Write a callback helper that runs a function for each array item. Reuse it with two different callbacks.
3. Write a variadic max function with `table.pack`. Forward its inputs to a logger first.
4. Return two values from a parser. Consume them, then show what extra parentheses do.
5. Iterate one array and one dictionary with generalized for. Guard one pcall around a failing call.

::: details 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
1
```

Each maker call owns a fresh `n`. The `a` counter reaches 2 while `b` starts at 1.
:::

::: details 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 1
```

One helper visits each item. Two callbacks produce two behaviors with no helper changes.
:::

::: details 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
9
```

Packing counts correctly, scanning finds the best, and `...` forwards from wrapper to worker.
:::

::: details 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
3
```

Two locals take both values. Extra parentheses keep only the first value, 3.
:::

::: details 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
true
```

Generalized 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 with `f(...)`. The token stays last-only.
* Multiple returns flow as tuples. Parentheses trim them.
* Iterate tables with `for k, v in t`. Remember `__iter` for custom containers.
* Guard expected failures with pcall and branch on its boolean.
* Next: [Modules and Require](./modules-and-require.md).
