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) |
a // b |
Floor division is an operator; math.floor(a / b) also works. |
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 unreliablelua55-length. 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 5lua55-truthy.
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 ?? 0mdn-nullish.
Division: / floats, // floors
Division is the other arithmetic trap. 7 / 2 is 3.5, even when both
operands are integers: / always returns a float. Integer division has its
own operator, // - floor division, which rounds toward minus infinity, not
toward zero. 7 // 2 is 3, and -7 // 2 is -4, where JS’s
Math.trunc(-7 / 2) gives -3lua55-arith. The % operator matches - it
is the remainder of floor division - so -7 % 2 is 1, not the -1 JS
gives. Both work in this site’s runner. 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 sixteen-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.
-
↩
Lua 5.5 Reference Manual §3.4.1 (lua.org/manual/5.5) -
/always converts its operands to floats and returns a float;//is floor division, rounding the quotient toward minus infinity;%is its matching remainder. -
↩
Lua 5.5 Reference Manual §2.1 (lua.org/manual/5.5) - “Both nil and false make a condition false; they are collectively called false values. Any other value makes a condition true” (0, the empty string, and empty tables included).
-
↩
MDN Web Docs, “Logical OR (||)” and “Nullish coalescing operator (??)” (developer.mozilla.org) - in JavaScript
0 || 5is5because 0 is falsy, so defaults that should accept 0 must use??, the hole the Lua idiom does not need. -
↩
Lua 5.5 Reference Manual §3.4.7 (lua.org/manual/5.5) -
#treturns “a border in that table”; a table with a hole has several borders and “is not a sequence,” in which case “#t can return any of its borders.”