atherhubather.hub
Back to Guides
Basics
9 min read

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.

Ather
Ather
Lead developer at Atherhub. Writes about Roblox internals, Luau, script engineering, and platform security.

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)
end

Luau 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 false and nil are 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)
end

Tables — 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
end

That += 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()) -- 5

The 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
Tip
Adding --!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 local and 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 #t to 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
Written by Ather

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.