luaguides

table.concat

table.concat(list [, sep [, i [, j]]])

table.concat joins a 1-indexed sequence of strings and numbers into a single string. It is the standard way to assemble a string from a list in Lua: pass the list, optionally with a separator and an index range, and get back the joined result.

Syntax

table.concat(list [, sep [, i [, j]]])

All four parameters are positional. sep, i, and j are optional and fall back to the defaults below. The typical call shapes are:

table.concat(list)                 -- default sep, default range
table.concat(list, ", ")           -- custom sep, default range
table.concat(list, ", ", 2, 5)     -- custom sep, custom range

Parameters

NameTypeDefaultDescription
listtable(required)A 1-indexed sequence. Non-numeric keys are ignored. Entries at positions i..j must all be strings or numbers.
sepstring""String placed between consecutive entries. Not added before the first or after the last entry.
iinteger1First index in the range to concatenate.
jinteger#listLast index in the range to concatenate. If i > j, returns the empty string.

j defaults to #list, which means table.concat inherits every quirk of the length operator. If your sequence has nil holes, pass j explicitly.

Return value

Returns a single string built as list[i] .. sep .. list[i+1] .. sep .. ... .. list[j]. The empty string "" is returned when i > j or when the list is empty. table.concat never returns nil under normal conditions, so you do not need a result or "" guard around a call.

Examples

Joining with a separator

Pick any string for the separator; it is concatenated verbatim, with no escaping or quoting applied. Common picks are -, , , and | for human-readable output.

local parts = {"Lua", "table", "concat"}
print(table.concat(parts, "-"))

The function runs each entry through the standard string conversion, then stitches the parts together with the separator between every adjacent pair. The example prints the three words joined by hyphens, with no quoting, no escaping, and no trailing separator:

Lua-table-concat

The separator goes between every pair of adjacent entries, never at the start or end. With three entries you get exactly two separators in the result.

Default separator is the empty string

When you omit sep, the function uses the empty string. Use this for joining characters or a list of fragments built up with no delimiter in mind.

local letters = {"a", "b", "c", "d"}
print(table.concat(letters))

Without a separator argument, Lua falls back to the empty string. That is the natural fit for joining characters, fragments, or any sequence where the parts have no delimiter of their own. The code joins the four letters with nothing inserted between them:

abcd

The default-separator form is also marginally the fastest, since the function does not have to insert anything between entries.

Numbers are auto-stringified

Each number is converted using the usual Lua rules: integers print without a decimal, and floats use the shortest round-trip representation. In Lua 5.4 this is predictable, so 0.1 + 0.2 prints as 0.3 rather than IEEE-754 noise.

local nums = {1, 2, 3, 4, 5}
print(table.concat(nums, ", "))

Each numeric entry goes through Lua’s default number-to-string conversion before it is appended to the result. The snippet builds a short sequence of positive integers and asks for a comma-space separator between them:

1, 2, 3, 4, 5

Mixing integers and floats in the same list is fine. If you need a specific format such as two decimal places, leading zeros, or hex output, format each number yourself with string.format first.

Slicing with i and j

Passing i and j lets you join a subrange of a sequence without copying it. The defaults are 1 and #list, so omitting them means the whole sequence.

local log = {"INFO", "WARN", "ERROR", "DEBUG"}
print(table.concat(log, " | ", 2, 3))

Passing both i and j lets you join a strict subrange of a sequence without copying the slice into a temporary table. The defaults are 1 and #list, so omitting them means the whole list, but you can pin either end. The example pulls a middle pair out of a log-level list:

WARN | ERROR

Indexing is inclusive on both ends, the same as the .. operator and string.sub. For a ring buffer whose live range wraps around the end, join two slices:

-- buf, sep, head, n, tail come from your ring buffer state
local s = table.concat(buf, sep, head, n) .. table.concat(buf, sep, 1, tail)

Empty input and i > j short-circuit

Both empty input and i > j short-circuit to "". That makes table.concat safe to call on a sequence you have not checked.

print("[" .. table.concat({}) .. "]")
print("[" .. table.concat({"a", "b"}, ",", 5, 1) .. "]")

Both the empty-list case and the i > j case fall through to the empty string, which makes table.concat safe to call on a sequence you have not validated. The first call has no entries, and the second asks for the range from index 5 down to index 1, which is reversed. Both reduce to the same "" result:

[]
[]

The i > j short-circuit is the safest way to guard a call when the table might be empty or you are slicing past its end. You do not need a separate length check.

Mixing numbers and strings

Mixing types in the same list works because each entry is converted to a string independently. The order of the entries in the result matches the order in the list.

local mixed = {"user_", 42, "_active"}
print(table.concat(mixed))

Mixing types inside a single list is fine because each entry is stringified independently when it is copied into the result buffer. The order of entries in the output matches the order of indices in the list, and the separator (here the default empty string) is inserted between every pair. The example weaves a string, an integer, and another string:

user_42_active

For booleans and other types, use tostring to get the same stringification behavior.

Element type error

The check is strict: anything that is not a string or a number raises. There is no __concat fallback to rescue you, even if you have defined a metamethod on a custom type.

local bad = {"a", true, "c"}
print(table.concat(bad, ","))

Raises an error such as invalid value (boolean) at index 2 in table for 'concat'. The exact message wording varies by Lua build, but the behavior is consistent: any entry that is neither a string nor a number errors out. Booleans, nil, tables, functions, threads, and userdata are all rejected.

Common pitfalls

No __concat metamethod is invoked. The .. operator falls back to __concat when its operands are tables or full userdata, but table.concat does not. The function only accepts raw strings and numbers.

local mt = { __concat = function() return "via metamethod" end }
local proxy = setmetatable({}, mt)

print(proxy .. " anything")                              -- via metamethod
print(table.concat({"raw", "strings"}, "-"))            -- raw-strings

If you have built a type that relies on __concat to format itself, you cannot use table.concat on a list of those values. Walk the list and call .. yourself, or pre-format each entry with tostring first.

The default j is #list. This means table.concat is sensitive to the usual length-operator caveats: a nil hole can cause the function to stop at an unpredictable boundary, or to error on a nil entry mid-range.

local sparse = {"a", "b", "c"}
sparse[2] = nil
print(table.concat(sparse, ","))                        -- "a" or an error

If your sequence might have holes, pass j explicitly: table.concat(sparse, ",", 1, 3) gives a defined result. The same rule applies to string.rep and any other function that depends on #.

Booleans raise, they do not coerce. tostring(true) is "true", but table.concat({true, false}) raises. Convert with tostring first if you need to. This is the most common cause of “table.concat errored and I do not know why” in real code, especially in scripts that build CSV from config tables.

It is not a recursive join. Nested tables in the list are an error, not a deep flatten:

local tree = {{"a"}, {"b"}}
print(table.concat(tree, ","))                          -- error

For tree-style serialization, walk the structure yourself before calling concat. The C implementation in ltablib.c works entry by entry, and __concat is never consulted on the entries.

Empty list is "", not nil. When composing output conditionally, check with == "" if that matters. Most code does not need to distinguish, but table.concat({}) == "" is true, and so is table.concat({}, ",", 5, 1) == "".

Performance: prefer table.concat to a .. chain. Building a long string with s = s .. t[i] inside a loop is the classic Lua performance anti-pattern: each iteration allocates a fresh string and copies the previous result. table.concat measures the total length first, allocates one buffer, then copies entries in. On a 1,000,000-entry list, the difference is roughly an order of magnitude.

local function join_path(parts)
  local buf = {}
  for i, part in ipairs(parts) do
    buf[i] = tostring(part)
  end
  return table.concat(buf, "/")
end

print(join_path({"", "usr", "local", "bin"}))           -- /usr/local/bin

See also