Luau Syntax Basics
Luau is Roblox's flavour of Lua — gradually typed, fast, and designed for the kind of sandboxed game scripting that Roblox does at scale. This guide walks through the parts of the language a new script author meets first: variables, control flow, tables, functions, a couple of object-oriented patterns, and the type system.
Why Luau exists
Roblox originally ran on standard Lua 5.1. As the platform grew, Roblox forked Lua into "Luau" so they could ship features their engine needed without waiting on the upstream Lua roadmap. The biggest practical changes Luau introduced are gradual typing, a faster bytecode VM, a string-interpolation syntax, generalised iteration, and a much stronger standard library for tables and strings.
The good news: if you've ever written Lua, almost all of it is still valid Luau. The new bits — types, continue, the{1, 2, 3}style table syntax — are additions, not replacements.
Variables and scoping
Variables in Luau are declared with local. Without that keyword, a variable becomes a global, which is almost always a mistake — globals leak between scripts, are slower to look up, and survive past the scope you wrote them in.
local playerName = "Atherhub"
local maxHealth = 100
local isAlive = true
-- Block scope: this 'temp' is only visible inside the do...end
do
local temp = playerName .. " is online"
print(temp)
endLuau is dynamically typed by default, so a single variable can hold a string, a number, a function, or a table over its lifetime. The type system (covered later) lets you opt into static checks where you want them without forcing them everywhere.
Control flow
The shapes of if, while, and for are all close to other languages, with two small surprises:
- Conditions don't need parentheses, but blocks must close with
end. - Only
falseandnilare falsy. Zero, empty strings, and empty tables are all truthy.
local hp = 35
if hp <= 0 then
print("dead")
elseif hp < 25 then
print("danger")
else
print("ok")
end
for i = 1, 5 do
print("tick", i)
end
-- Generalised iteration over any iterable (Luau-only)
local fruits = { "apple", "pear", "fig" }
for index, value in fruits do
print(index, value)
endTables — the only data structure
Tables are the single data structure in Lua/Luau. They function as arrays, dictionaries, sets, records, and objects — depending on how you key them.
-- Array-like
local colors = { "red", "green", "blue" }
print(colors[1]) -- "red" (Lua is 1-indexed)
-- Dictionary-like
local player = {
name = "Atherhub",
level = 42,
inventory = { "sword", "potion" },
}
print(player.name, player.level)
-- Mixing both works too
local mix = { "first", "second", style = "purple" }Two things to remember when you start: arrays are 1-indexed (not 0-indexed), and Luau's #table length operator only works reliably on dense arrays. If you mix string keys and numeric keys, count manually.
Functions
Functions are first-class values: you can pass them around, store them in tables, return them from other functions, and assign them to variables.
local function greet(name)
return "Hello, " .. name
end
print(greet("Atherhub"))
-- Multiple return values
local function bounds(t)
return t[1], t[#t]
end
local first, last = bounds({ 10, 20, 30 })
-- Variadic
local function sum(...)
local total = 0
for _, n in { ... } do total += n end
return total
endThat += is Luau-specific. Standard Lua doesn't have compound assignment operators — Luau added them along with -=, *=, etc.
Objects with metatables
Lua doesn't have classes, but you can fake them cleanly with metatables. The trick: store methods on one table, then point each instance at it via __index.
local Vector = {}
Vector.__index = Vector
function Vector.new(x, y)
return setmetatable({ x = x, y = y }, Vector)
end
function Vector:length()
return math.sqrt(self.x * self.x + self.y * self.y)
end
local v = Vector.new(3, 4)
print(v:length()) -- 5The colon syntax (v:length()) is sugar for v.length(v) — Luau automatically passes self as the first argument when you call with a colon.
Types (opt-in)
Luau's type system is gradual: types are optional, but they get checked by the Roblox Studio analyser when you provide them. They don't change runtime behaviour, so you can sprinkle them incrementally without breaking anything.
type Player = {
name: string,
level: number,
inventory: { string },
}
local function describe(p: Player): string
return p.name .. " (lvl " .. p.level .. ")"
end
-- Union types
local function format(value: string | number): string
return tostring(value)
end--!strict at the top of a script enables strict type checking for that file. You usually start a project in non-strict mode, then turn on strict file-by-file as you stabilise APIs.Common beginner pitfalls
- Forgetting
localand accidentally polluting the global table. - Confusing
.with:when calling methods on an object. - Using
==on tables. Tables compare by reference, not value, so two structurally identical tables are not equal. - Expecting
#tto count keys in a dictionary. It only counts contiguous numeric keys starting at 1.
Where to go next
Once syntax feels comfortable, the next two things worth learning are Roblox's service system (game:GetService), which is how scripts talk to the engine, and event signals (RBXScriptSignal), which are how everything reactive in Roblox is wired up. After that, the standard library reference on the Roblox creator docs is the most useful single page you'll bookmark.
Ather is the lead developer behind Atherhub. He's been writing Luau and Roblox tooling for the better part of a decade, with a focus on the messy interface between game-script internals and the platforms that host them. Have feedback on this article? Drop it in the Discord.