Short answer: Luau is the gradually typed language derived from Lua 5.1 that Roblox uses. It adds optional type annotations (local port: number = 8080), --!strict checking, generics, string interpolation with backticks, continue, compound assignment (+=), if expressions, iteration without pairs, and library extras such as table.find and string.split. It drops goto, the io library and 64-bit integers. The tables below cover the syntax and the first Roblox APIs you will meet.
The language examples in this Luau cheat sheet were run on the Luau 0.740 VM on 9 October 2026. That run checks syntax and runtime behaviour, not type errors; type-checker rules and the Roblox section were checked against the official documentation.
Table of Contents
Luau vs Lua 5.1 at a glance


| Feature | Lua 5.1 | Luau |
|---|---|---|
| Types | Dynamic only | Optional annotations, inference, --!strict |
| String interpolation | string.format | `Host {name}:{port}` |
| Skip an iteration | No continue (5.2+ uses goto) | continue; goto is not supported |
| Compound assignment | n = n + 1 | n += 1, ..=, //= and others |
| Conditional expression | a and b or c | if a then b else c |
| Iterate a table | pairs(t) / ipairs(t) | for k, v in t do (pairs and ipairs still work) |
| Floor division | math.floor(a / b) | a // b |
| Numbers | Double | Double only (exact integers up to 2^53); no 5.3 integer subtype |
| Bitwise | None | bit32 library; no & or | operators |
| Files and processes | io, os.execute | Removed by sandboxing; os keeps time, clock, date, difftime |
| Finalisers | __gc | Not supported |
| Constants | None | const LIMIT = 10 in recent Luau releases |
Luau keeps getfenv and setfenv for compatibility, but using them disables some optimisations. Roblox also disables loadstring unless ServerScriptService.LoadStringEnabled is turned on, and then only on the server.
Variables, operators and string interpolation
| Syntax | What it does | Example |
|---|---|---|
local x: number = 1 | Local with an optional type | local name: string = "web01" |
const X = 10 | Binding that cannot be reassigned (the value can still change if it is a table) | const MAX_PLAYERS = 12 |
`… {expr} …` | Interpolated string; \{ for a literal brace | print(`sum {1 + 2}`) |
+=, -=, *=, /=, //=, %=, ^=, ..= | Compound assignment (statements, not expressions) | score += 10 |
if c then a elseif d then b else e | If expression; else is required | local label = if port == 443 then "https" else "http" |
1_000_000, 0b1010, 0xFF | Digit separators, binary and hex literals | local mask = 0b_0101_0101 |
typeof(x) | Like type, but names engine types ("Instance", "Vector3" on Roblox) | typeof(part) == "Instance" |
.., #, ~=, and/or/not | As in Lua 5.1 | local v = x or "default" |
Backtick strings must be wrapped in parentheses when passed to a function: print(`hi`) works, print`hi` does not. A double brace {{ inside a backtick string is a parse error.
Type annotations
| Type syntax | Meaning | Example |
|---|---|---|
number, string, boolean, nil, any, unknown, never | Built-in types | local ok: boolean = true |
T? | Optional: same as T | nil | local nick: string? = nil |
A | B, A & B | Union and intersection | local id: string | number = 5 |
{number} | Array of numbers | local scores: {number} = {1, 2, 3} |
{[string]: number} | Dictionary | local counts: {[string]: number} = {} |
{x: number, y: number} | Table with named fields | type Point = {x: number, y: number} |
{read x: number} | Read-only property | Writes are type errors |
"up" | "down" | String literal types | type Dir = "up" | "down" |
(number, string) -> boolean | Function type | type Callback = (number) -> () |
(): (number, string) | Multiple return values | local function pair(): (number, string) |
...: number | Typed varargs | local function sum(...: number): number |
expr :: T | Type cast | local s = value :: string |
typeof(expr) in a type | Type of an existing value (not evaluated at runtime) | type Config = typeof(DEFAULTS) |
export type | Make a type visible to modules that require this one | export type Server = {host: string} |
--!strict
type Pair<T> = {first: T, second: T}
local function add(a: number, b: number): number
return a + b
end
local function first<T>(list: {T}): T?
return list[1]
end
local p: Pair<number> = {first = 1, second = 2}
print(add(1, 2), first({"a", "b"}), p.first) --> 3 a 1
| Mode (first line) | Behaviour |
|---|---|
--!nocheck | No type checking for the file |
--!nonstrict | Default. Unknown types become any, so fewer errors are reported |
--!strict | Full inference; catches mismatches such as adding a string to a number |
Narrow a union with a check before use: inside if typeof(v) == "string" then … end the checker treats v as a string, and after assert(x) an optional x is no longer nil.
Tables and the standard library extras
| Function | What it does | Example |
|---|---|---|
table.find(t, v) | Index of a value in an array, or nil | table.find({"a", "b"}, "b") gives 2 |
table.create(n, v) | Pre-sized array filled with v | table.create(3, 0) |
table.clear(t) | Empty a table but keep its capacity | table.clear(cache) |
table.clone(t) | Shallow copy | local copy = table.clone(defaults) |
table.freeze(t) / table.isfrozen(t) | Make read-only (shallow); writes raise an error | local CONFIG = table.freeze({…}) |
string.split(s, sep) | Split into an array | string.split("a,b,c", ",") |
math.clamp(x, min, max) | Limit to a range | math.clamp(15, 0, 10) gives 10 |
math.round(x), math.sign(x) | Round half away from zero; sign | math.round(2.5) gives 3 |
bit32.band, bxor, lshift | Bitwise operations on 32-bit values | bit32.band(12, 10) gives 8 |
buffer.create(n) | Fixed-size byte buffer with typed reads and writes | buffer.writeu32(b, 0, 42) |
utf8.len(s) | Character count | utf8.len("héllo") gives 5 |
Control flow and generalised iteration
local cfg = {host = "db01", port = 3306}
for key, value in cfg do -- no pairs() needed
print(key, value)
end
for i = 1, 5 do
if i % 2 == 0 then continue end -- continue must be the last statement in its block
print(i) --> 1, 3, 5
end
Plain tables iterate 1..#t in order first, then the remaining keys in no set order. Give a table or class an __iter metamethod to control what for … in obj do returns. while, repeat … until, numeric for and break work as in Lua 5.1.
Functions, errors and coroutines
| Syntax | What it does | Example |
|---|---|---|
local function f(a: number): string | Typed local function | local function greet(n: string): string |
function<T>(x: T): T | Generic function | local function first<T>(list: {T}): T? |
error(msg, 0) | Raise without the location prefix | error("bad port", 0) |
pcall(f, ...) | Returns true, results or false, err | local ok, err = pcall(fetch) |
xpcall(f, handler, ...) | With a message handler; extra arguments go to f | xpcall(f, debug.traceback) |
coroutine.wrap(fn) | Generator function | coroutine.yield(i) inside |
coroutine.close(co) | Close a suspended coroutine | From Lua 5.4, supported in Luau |
@native, @deprecated | Function attributes | @native local function hot() … end |
Luau does not perform tail calls, so deep recursion that relies on them in standard Lua will hit the stack limit (about 20,000 Lua calls per thread).
Modules and typed classes
local Account = {}
Account.__index = Account
type AccountData = {owner: string, balance: number}
export type Account = typeof(setmetatable({} :: AccountData, Account))
function Account.new(owner: string): Account
return setmetatable({owner = owner, balance = 0}, Account)
end
function Account.deposit(self: Account, amount: number)
self.balance += amount
end
function Account:describe(): string
return `{self.owner}: {self.balance}`
end
return Account -- in a ModuleScript; callers use require(path.to.Account)
The typeof(setmetatable({} :: Data, Class)) pattern gives the checker both the fields and the methods. Annotating self explicitly, as in deposit, helps in --!strict files. Another module refers to the exported type as Account.Account after local Account = require(…).
Roblox basics
| API | What it does | Example |
|---|---|---|
| Script / LocalScript / ModuleScript | Runs on the server / runs on the client / returns a value to require | Server logic in ServerScriptService |
game:GetService(name) | Get a service | local Players = game:GetService("Players") |
Instance.new(class) | Create an object | local part = Instance.new("Part") |
obj.Parent = … | Put it in the data model | part.Parent = workspace |
:WaitForChild(name) / :FindFirstChild(name) | Wait for / look up a child (nil if missing) | script.Parent:WaitForChild("Handle") |
event:Connect(fn) | Listen to an event; returns a connection :Disconnect() | part.Touched:Connect(onTouch) |
task.wait(s) | Yield; returns the time actually waited | Use instead of the deprecated wait() |
task.spawn(f), task.delay(s, f), task.defer(f), task.cancel(t) | Schedule threads | task.delay(5, cleanup) |
RemoteEvent:FireServer(…) / .OnServerEvent | Client-to-server messages; the server receives the player first | remote.OnServerEvent:Connect(function(player, data) … end) |
local Players = game:GetService("Players")
Players.PlayerAdded:Connect(function(player: Player)
print(`{player.Name} joined`)
end)
Treat everything a client sends through a RemoteEvent as untrusted input: check types and ranges on the server before acting on it. Wrap DataStore calls such as GetAsync in pcall, because they can fail on network or quota errors.
Official documentation (checked 9 October 2026): Luau syntax by example · An introduction to Luau types · Luau compatibility with Lua · Luau standard library · Roblox Creator Hub: Luau · Roblox Creator Hub: task library
Related guides: Lua Cheat Sheet: Lua 5.4 Syntax, Tables and Patterns · TypeScript Cheat Sheet: Types, Generics and tsconfig.
Reading a lot of our guides? Go ad-free with srvScripts Pro for $6 a month. It also pays for new guides. Cancel any time.