luaguides

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; calling math.max with no arguments raises an error.
  • ... (number, variadic): zero or more additional numeric values to compare against x. Any operand that cannot be coerced to a number raises bad 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:

InputsReturn subtype
All integersinteger
All floatsfloat
Mixed integer and floatfloat
Strings that parse as numberscoerced; subtype follows the rule above
Non-numeric stringerror
Zero argumentserror

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