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 Introduction - the eleven-lesson series: one idea at a time, each check graded by the real runner.
- Reading Elixir as a JS developer - the same cheat-sheet treatment for the site’s other language, so the two mappings sit side by side.
- Stacks, queues, and associative arrays - the “one structure, three jobs” idea the table embodies.
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.