Reading Lua as a JS developer

LLM-authored, human-reviewed

Foundations

Article title: Reading Lua as a JS developer

— Article body (markdown) — Lua is the small language you keep finding in config files (Neovim, AwesomeWM, Hammerspoon), game scripting (Roblox, LÖVE, World of Warcraft addons), and embedded automation. It was designed to be tiny - the reference implementation is a few hundred kilobytes. That choice shapes the language: one data structure, a handful of keywords, and syntax that reads like a cleaner C. This cheat sheet shows you how to read and write Lua as a JavaScript developer. It supports the Lua Introduction series on this site. It is not a full tutorial; the series teaches that, lesson by lesson.

The core mapping

JavaScript Lua Notes
const x = 5; / let x = 5; local x = 5 No const; any variable is reassignable. Always write local.
a !== b a ~= b Not-equals is ~=, not !=.
a && b, a || b, !a a and b, a or b, not a Boolean operators are words.
Math.floor(a / b) math.floor(a / b) The math library, not Math.
arr.length #t # is the length operator.
"a" + "b" "a" .. "b" Concatenation is .., not +.
else if (x) elseif x then One word, no space - else if silently nests and needs its own end.
for (let i = 0; i < n; i++) for i = 1, n do Numeric for; 1-based, inclusive.
arr.map(fn) / arr.filter(fn) for i = 1, #t do ... end No map/filter; you write the loop.
if (x) { a } else { b } if x then a else b end then/end keywords replace braces.

The first thing to notice is what Lua leaves out: no braces, no semicolons, and no parentheses around conditions. Keywords - then, do, end - delimit blocks. Indentation helps you read the structure, but those keywords define it.

The one habit that will trip you up: one table, indexed from 1

JavaScript gives you arrays, objects, Maps, and Sets - four structures for four jobs. Lua gives you exactly one, the table, and uses it for all four. As an array, it is a list literal:

local nums = {10, 20, 30}

As a map, a table takes string keys. Dot syntax is shorthand for brackets: counts.a is exactly counts["a"], just as JS uses obj.key and obj["key"] interchangeably. The site’s Stacks, queues, and associative arrays article explains the “one structure, three jobs” idea. In Lua, that abstraction is literally the only data structure in the language.

Memorize the indexing rule. Tables are 1-based: nums[1] is the first element, while nums[0] is not an error; it is nil (Lua’s “nothing here” value). #nums gives the length, and the standard loop is for i = 1, #nums do ... nums[i] ... end. Two consequences matter. First, translate every JS loop with bounds shifted by one. Second, # counts the contiguous run from index 1 - put a nil hole in the middle of a table and the length becomes unreliable. Keep array-tables dense: always append with table.insert(t, v) or t[#t + 1] = v; never skip indexes.

Truthiness: only nil and false are false

This trap causes real debugging time. In JavaScript, 0, "", and null are all falsy, so 0 || 5 is 5. In Lua, only nil and false are falsy. 0, the empty string, and an empty table are all truthy. So 0 or 5 is 0, not 5.

The useful part is what makes the idiom work: and and or return a value, not a boolean. a and b evaluates to b when a is truthy; otherwise it evaluates to a. a or b evaluates to a when a is truthy; otherwise it evaluates to b. That is JavaScript’s short-circuit behavior, and it powers the counting idiom used throughout the catalog:

counts[k] = (counts[k] or 0) + 1

Read it as “Counts under k, defaulting to 0, plus one.” The or 0 supplies the default, just as x || 0 would in JS. Lua’s truthiness rule makes this version correct where the JS version would need ?? 0.

Division is always float division

Division is the other arithmetic trap. 7 / 2 is 3.5, even when both operands are integers; Lua has no integer division operator. Use math.floor(a / b). The math library is a plain table of functions: math.floor(3.7) is 3, math.max(2, 9) is 9, and math.min(2, 9) is 2. The catalog’s Lua solutions use these three constantly.

Multiple return values

Lua functions can return several values at once. The caller receives them with multiple assignment:

local function divmod(a, b)
  local q = math.floor(a / b)
  return q, a - q * b
end

local q, r = divmod(17, 5)   -- q is 3, r is 2

There is no destructuring or wrapper object. Values line up from left to right; extras are dropped, and missing values become nil. The string library uses this: string.match("09:30", "^(%d+):(%d+)") returns two captures, which you catch the same way. One runner detail matters: this site grades a single return value, so a check that produces two numbers hands them back as a table - return {q, r}.

Strings, in brief

Lua strings are immutable, quoted, and 1-indexed like tables. Concatenation is ... The string library lives on the string table - string.sub(s, 2, 4), string.find(s, ":"), string.format("%02d", n) - and each function has colon-call sugar: s:sub(2, 4) means string.sub(s, 2, 4). The catalog uses pattern matching most. Lua patterns are a smaller language than regex: %d is a digit (%d+ means one or more), ^ anchors the start, and parentheses capture. The full treatment is in the Strings article. Remember one rule: Lua’s patterns are not regex, so \d+ and \s from the regex world do not translate.

The runner quirks (specific to this site)

Two things work in real Lua but fail in the Luerl interpreter this site runs. Learn them before they cost you a submission. First, string.gmatch, the pattern iterator, is broken here; it raises on any pattern. Restructure the code around string.match with anchors and captures. That works fine. Second, recursive functions cannot use the local function name(...) sugar - the inner call fails with an “undefined function” error. Declare the local first, then assign it:

local factorial
factorial = function(n)
  if n <= 1 then return 1 end
  return n * factorial(n - 1)
end

With declared-then-assigned, the name is in scope inside its own body. Every recursive Lua solution in the catalog uses this form.

Where to go next

Lua is small by design. Learn the two habits - one table, indexed from 1 - and the rest is syntax you already know, with fewer braces.

Related exercises

← Back to articles