math.max
math.max(x, ...) math.max is a standard library function in Lua 5.4 that returns the largest of its numeric arguments. You pass one required number followed by zero or more additional numbers, and the function gives you back the maximum of the set. The function lives in the math module, which loads automatically, so there is no require step. Its counterpart, math.min, returns the smallest value and pairs with math.max for range checks, clamping, and reductions.
Syntax and return value
The signature is variadic: the first argument is named, the rest come through ....
math.max(x, ...)
x(number, required): the first candidate value. There is no default; callingmath.maxwith no arguments raises an error....(number, variadic): zero or more additional numeric values to compare againstx. Any operand that cannot be coerced to a number raisesbad argument #N to 'max' (number expected, got <type>).
The return value is a single number: the largest of the supplied arguments. Lua’s dual number subtype (integer and float) determines what comes back, following the standard “integer/float” rule used across the math library:
| Inputs | Return subtype |
|---|---|
| All integers | integer |
| All floats | float |
| Mixed integer and float | float |
| Strings that parse as numbers | coerced; subtype follows the rule above |
| Non-numeric string | error |
| Zero arguments | error |
So math.max(1, 2, 3) returns the integer 3, while math.max(1, 2.5) returns the float 2.5. The rule is shared with math.min, math.abs, and most other math module functions.
Basic examples
Two-argument usage is the bread-and-butter case, and it shows up wherever you compare two values and keep the larger:
print(math.max(1, 2)) --> 2
print(math.max(-3.5, -1.2)) --> -1.2
print(math.max(1, 2.5)) --> 2.5
print(type(math.max(1, 2))) --> integer
print(type(math.max(1, 2.5))) --> float
The variadic form accepts any number of trailing arguments in a single call, which is useful when comparing several candidates at once without writing a loop, and the order of arguments does not matter:
print(math.max(10, 20, 5, 30, 15)) --> 30
print(math.max(7)) --> 7
print(math.max(-1)) --> -1
A single argument is returned unchanged. Unlike Python’s max(iterable) or JavaScript’s Math.max(...arr), Lua’s math.max only sees the values you pass explicitly; it never unpacks a table for you.
Common patterns
Clamping a value into a range
The most common real-world use of math.max is the lower bound of a clamp helper. Pair it with math.min to keep a value inside an inclusive range:
local function clamp(x, lo, hi)
return math.min(math.max(x, lo), hi)
end
print(clamp(15, 0, 10)) --> 10
print(clamp(-5, 0, 10)) --> 0
print(clamp(7, 0, 10)) --> 7
You will see this idiom in game code, UI layout, and anywhere a value needs to stay between two bounds. Reading inside-out, math.max(x, lo) raises x to the lower bound, and the outer math.min caps it at the upper bound.
-- Damage after armor, clamped to [0, 9999]
local function apply_armor(raw_damage, armor)
return clamp(raw_damage - armor, 0, 9999)
end
print(apply_armor(50, 30)) --> 20
print(apply_armor(5, 30)) --> 0 -- would have been -25 without clamp
print(apply_armor(15000, 5)) --> 9999 -- capped at the upper bound
Bounded counter
Resource pools, cooldowns, and stock counters all share the same problem: subtracting past zero. math.max makes a clean floor for a counter that should never go negative:
local count = 0
local function decrement()
count = math.max(0, count - 1)
end
decrement()
decrement()
decrement()
print(count) --> 0
Three decrements from zero leave count at zero. Without the floor, subtracting from zero would dip into negative integers and surface the bug only when something downstream assumed a non-negative value.
-- Cooldown timer that cannot run below zero
local cooldown = 30
local function tick()
cooldown = math.max(0, cooldown - 1)
end
for _ = 1, 35 do tick() end
print(cooldown) --> 0
Max element of a table
Finding the largest element in a sequence is a common task, but math.max does not accept a table directly. You have to spread the elements yourself with table.unpack:
local t = {3, 1, 4, 1, 5, 9, 2, 6}
print(math.max(table.unpack(t))) --> 9
For large tables or hot paths, an explicit accumulator avoids the variadic spread and the per-call allocation that comes with it. A small loop keeps the comparison tight and reads more like idiomatic Lua:
local function max_of(t)
local m = t[1]
for i = 2, #t do
if t[i] > m then m = t[i] end
end
return m
end
print(max_of({3, 1, 4, 1, 5, 9, 2, 6})) --> 9
print(max_of({-7, -2, -10})) --> -2
The accumulator is a heuristic choice for hot loops; table.unpack is fine for one-off calls.
Edge cases and gotchas
Several behaviors come from Lua’s number semantics, not from math.max itself. Knowing them up front saves you from debugging “why did this return NaN” or “why did this error” later.
Zero arguments raise an error
math.max() with no arguments does not return nil or -math.huge. It raises:
-- math.max() with no arguments raises:
-- bad argument #1 to 'max' (number expected, got no value)
local ok, err = pcall(math.max)
print(ok) --> false
print(err:match("bad argument")) --> bad argument
Wrap calls in pcall when the argument count is uncertain.
NaN poisons the result
Lua follows IEEE 754, where any comparison with NaN is false. So math.max(0/0, 1) returns NaN, not 1:
print(math.max(0/0, 1)) --> nan
-- NaN is the only value not equal to itself
local function is_nan(x)
return x ~= x
end
print(is_nan(0/0)) --> true
print(is_nan(1)) --> false
Guard values before passing them to math.max if NaN is possible.
math.huge
math.huge is positive infinity, so math.max(math.huge, 1) returns math.huge. Negative infinity (-math.huge) loses to any finite number:
print(math.max(math.huge, 1)) --> inf
print(math.max(-math.huge, 7)) --> 7
print(math.max(math.huge, math.huge)) --> inf
-- Useful as a starting sentinel when finding a maximum
local function max_from(start, t)
local m = -math.huge
for i = start, #t do
if t[i] > m then m = t[i] end
end
return m
end
print(max_from(1, {-3, 7, 2, 9})) --> 9
Mixed integer and float
Mixing integer and float arguments in a single call promotes the result to float, which matches Lua’s broader arithmetic coercion rule. If you need an integer back, coerce explicitly with math.floor:
print(math.max(1, 2.5)) --> 2.5
print(math.max(1, math.floor(2.7))) --> 2
print(type(math.max(1, 2.5))) --> float
print(type(math.max(1, math.floor(2.7)))) --> integer
This matters when downstream code does integer-only operations like bitwise shifts (<<, >>, &, |) or asserts on the subtype.
String coercion
Strings that parse as numbers are coerced silently. Strings that cannot parse raise an error:
print(math.max("10", 5)) --> 10
print(math.max("3.14", 2)) --> 3.14
-- math.max("x", 1) raises: bad argument #1 to 'max' (number expected, got string)
-- Safe pattern for untrusted input: convert first, then check for nil
local function safe_max(a, b)
a = tonumber(a)
b = tonumber(b)
if not a or not b then return nil end
return math.max(a, b)
end
print(safe_max("42", "13")) --> 42
print(safe_max("oops", 5)) --> nil
Convert with tonumber first and check for nil at any boundary where the data is not strictly typed.
See also
math.absfor magnitude in signed rangesmath.floorfor integer truncation when you need a subtype fixmath.ceilfor the rounding neighbortable.unpackfor spreading a table into variadic arguments- Variables and types tutorial for integer vs float context
- Control flow tutorial for the if-comparisons
math.maxoften replaces - Table sorting guide for using min and max as sort keys