luaguides

math.ceil

math.ceil(x)

math.ceil(x) returns the smallest integer greater than or equal to x. The Lua 5.4 manual calls this “the ceiling of x”, and the rule is unambiguous: for any finite input, the result is the unique integer n such that n - 1 < x <= n. For inputs that already are integers, the same value comes back with its integer subtype preserved.

It looks like a one-line function, and it is. The interesting parts are the gotchas: negatives round toward +infinity (not away from zero), results out of integer range come back as floats, and there is no second argument for “round to N decimals”. This page covers the behavior, the edge cases, and the patterns people reach for when math.ceil is the right primitive.

Signature

math.ceil(x)

Parameters

math.ceil takes a single required argument.

ParameterTypeDefaultDescription
xnumber (integer or float)requiredThe value to round up. Numeric strings are auto-coerced to numbers; anything else raises a bad argument #1 to 'ceil' (number expected, got ...) error.

Return value

Returns the smallest integer greater than or equal to x. The result is the integer subtype whenever the value fits inside the representable integer range (math.mininteger to math.maxinteger). Outside that range the function falls back to a float, since Lua cannot represent the outcome as an integer. For inputs that already are integers, the subtype passes through unchanged.

Three behaviors worth memorising:

  • math.ceil(3) returns the integer 3, not the float 3.0. The subtype of the input is preserved, which matters for code that branches on math.type(x) == "integer".
  • math.ceil(-3.3) returns -3, not -4. “Ceiling” means “smallest integer greater than or equal to x”, and for negative inputs that is the value closer to zero.
  • math.ceil(1e100) returns a float near 1e100, since the result is far beyond math.maxinteger. The Lua 5.4 manual calls this out explicitly as the reason the function’s return type is annotated (integer, float) rather than just integer.

Examples

Positive values

The straightforward case. Positive fractional inputs round up to the next integer, and integer inputs pass through with their subtype preserved.

print(math.ceil(0.5))                  -- 1
print(math.ceil(1.0))                  -- 1
print(math.ceil(1.99))                 -- 2
print(math.ceil(3))                    -- 3
print(math.type(math.ceil(3)))         -- "integer"

The last line is the subtype-preservation check. If your downstream code distinguishes int from float (some serializers and JSON encoders do), math.ceil is a safe way to assert an integer-shaped result from a possibly-float input without going through math.tointeger.

Negative values round toward +infinity

This is the most common confusion point. Beginners hear “ceil” and assume “away from zero”, but the spec says “smallest integer greater than or equal to x”, which for negative numbers means closer to zero.

print(math.ceil(-3.3))        -- -3
print(math.ceil(-0.1))        -- 0
print(math.ceil(-5))          -- -5

If you want “away from zero” instead (the behavior some languages confusingly call ceil for negative inputs, since it does not match the spec wording), compose the helper yourself. The trick is to branch on the sign of x and dispatch to either math.floor or math.ceil, depending on which side of zero you started from:

local function ceil_away_from_zero(x)
    if x >= 0 then return math.ceil(x) end
    return math.floor(x)
end

print(ceil_away_from_zero(-3.3))   -- -4
print(ceil_away_from_zero(3.3))    -- 4
print(ceil_away_from_zero(0))      -- 0

Most code does not want that, which is why math.ceil is specified the way it is.

Rounding up to a step

The most common pattern built on math.ceil is grid-snapping: round x up to the nearest multiple of step. Useful for aligning positions, sizing buffers, and pagination.

local function ceil_to(x, step)
    return math.ceil(x / step) * step
end

print(ceil_to(7, 5))          -- 10
print(ceil_to(7.3, 0.5))      -- 7.5
print(ceil_to(-0.2, 1))       -- 0
print(ceil_to(0, 0.1))        -- 0

Watch the floating-point accumulation. If you do many ceil-to-step operations in a hot loop, the final result can drift by an ULP or two from what you expect. For graphics and physics that is usually fine; for financial code, switch to integer cents and do the arithmetic on those.

Edge cases: huge and infinities

math.ceil is defined for every finite input and for the infinities. NaN is the only case where the result is undefined by the spec, and in practice you get NaN back, since IEEE-754 ceil of NaN is NaN.

print(math.ceil(math.huge))           -- inf
print(math.ceil(-math.huge))          -- -inf
print(math.ceil(1e18))                -- 1000000000000000000
print(math.ceil(1e100))               -- 1e100

The 1e18 line still fits in a 64-bit integer (the standard Lua 5.4 integer width), so it comes back as the integer 1000000000000000000. 1e100 is far outside that range, so it comes back as a float. The fallback is silent; if your code distinguishes the two subtypes, branch on math.type after the call.

Behavior notes

math.ceil rounds toward +infinity, never toward zero. The negative-number behavior follows from the spec wording “smallest integer greater than or equal to x”, not from any intuition about the word “ceiling”. When writing documentation or reviewing code, keep that distinction explicit; a function called ceil is easy to misread.

One argument only. There is no math.ceil(x, decimals) overload like in some other languages. To round to N decimals you compose the call yourself:

local function ceil_to_decimals(x, decimals)
    local f = 10 ^ decimals
    return math.ceil(x * f) / f
end

print(ceil_to_decimals(3.14159, 2))   -- 3.15
print(ceil_to_decimals(3.14159, 3))   -- 3.142
print(ceil_to_decimals(0.001, 2))     -- 0.01
print(ceil_to_decimals(-1.234, 1))    -- -1.2

The result is a float and can suffer the usual floating-point rounding noise at the boundary, so do not use this for money. For decimal rounding on currency, work in integer cents.

Type errors are the only documented failure mode:

print(math.ceil("3.14"))     -- 4      (string coerces to number)
-- math.ceil("foo")          -- error: bad argument #1 to 'ceil' (number expected, got string)
-- math.ceil(nil)            -- error: bad argument #1 to 'ceil' (number expected, got nil)
-- math.ceil(true)           -- error: bad argument #1 to 'ceil' (number expected, got boolean)

If the input comes from untrusted sources such as environment variables, config files, or network payloads, validate with tonumber first or wrap the call in pcall and recover:

local function safe_ceil(input)
    local n = tonumber(input)
    if n == nil then return nil, "not a number: " .. tostring(input) end
    return math.ceil(n)
end

print(safe_ceil("3.14"))     -- 4
print(safe_ceil("foo"))      -- nil   not a number: foo
print(safe_ceil(nil))        -- nil   not a number: nil

The float fallback for out-of-range inputs is silent. A result of 1e100 is a float even though the input was an integer; math.type(result) returns "float". Branch on math.type after the call when the subtype matters downstream:

local function strict_ceil(x)
    local r = math.ceil(x)
    if math.type(r) == "integer" then return r end
    return nil, "result out of integer range: " .. tostring(r)
end

print(strict_ceil(1e18))     -- 1000000000000000000
print(strict_ceil(1e100))    -- nil   result out of integer range: 1e100

math.ceil is the building block for “round up by a step”, but it is not a general-purpose round. Lua has no built-in math.round; the idiomatic half-away-from-zero round for positive values is math.floor(x + 0.5). For negative inputs that formula needs adjustment, and it is worth deciding on a convention per codebase instead of picking whichever formula you happen to write.

See also

  • math.abs(): sibling math reference; same integer/float subtype rules.
  • tonumber(): explicit numeric coercion, useful before passing untrusted input to math.ceil.
  • pcall(): wrap math.ceil when the input might be a non-numeric string.