Modern, type-safe Luau for Roblox, covering --!strict and the new type solver, generics, string interpolation, if-expressions, task library, require-by-string, module/OOP patterns, error handling with retries, buffer/vector, and legacy APIs to avoid. Use when writing, refactoring, or explaining any Roblox script, fixing type errors, or modernizing old Lua code.
Installs into .claude/skills of the current project.
Are you the author of Roblox Luau?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/el4cteo-roblox-luau)
---
name: roblox-luau
description: Modern, type-safe Luau for Roblox, covering --!strict and the new type solver, generics, string interpolation, if-expressions, task library, require-by-string, module/OOP patterns, error handling with retries, buffer/vector, and legacy APIs to avoid. Use when writing, refactoring, or explaining any Roblox script, fixing type errors, or modernizing old Lua code.
---
# Modern Luau for Roblox
Luau is Roblox's gradually-typed Lua 5.1 derivative. Engine and language move fast; many
patterns from older tutorials (and older training data) are deprecated. Apply these defaults
to **every** script you write unless the project clearly uses another convention.
## Defaults for all generated code
1. `--!strict` at the top of new ModuleScripts (and Scripts when practical). Type public functions.
2. Get services with `game:GetService("Name")` at the top of the file, never `game.Name`.
3. Use the `task` library: `task.wait`, `task.spawn`, `task.defer`, `task.delay`, `task.cancel`.
Never `wait`, `spawn`, `delay` (deprecated, throttled, imprecise).
4. Yielding engine calls that can fail (DataStore, HTTP, Marketplace, Teleport, `*Async`) go inside `pcall`.
5. Prefer the `*Async` variants of APIs; Roblox renamed most yielding methods
(`LoadCharacterAsync`, `GetProductInfoAsync`, `UserHasBadgeAsync`, ...). See the table below.
6. Disconnect connections / destroy instances you create (see "Cleanup" below).
7. Naming: `PascalCase` for services, modules, classes, types, and instance names;
`camelCase` for locals and functions; `SCREAMING_SNAKE_CASE` for constants.
8. Tabs for indentation (StyLua default in the Roblox ecosystem), double quotes.
## Modern syntax cheat sheet
```luau
local Players = game:GetService("Players")
local count = 0
count += 1 -- compound assignment: += -= *= /= //= %= ^= ..=
local label = if count > 1 then "many" else "one" -- if-expression (no `and/or` pitfalls)
print(`Player count: {#Players:GetPlayers()} ({label})`) -- string interpolation
for index, player in Players:GetPlayers() do -- generalized iteration: no pairs/ipairs needed
if player.AccountAge < 1 then
continue -- continue is a keyword
end
print(index, player.Name)
end
local half = 7 // 2 -- floor division → 3
local frozen = table.freeze({ Speed = 16 }) -- read-only table; writes throw
local list = table.create(100, 0) -- preallocated array
local copy = table.clone(frozen) -- shallow copy (unfrozen)
local mid = math.lerp(0, 10, 0.5) -- also: math.map, math.clamp, math.round, math.sign
print(half, list[1], copy.Speed, mid)
```
- `pairs`/`ipairs` still work; generalized iteration is preferred and iterates arrays in order.
- `#t` is only defined for arrays without holes. Setting `t[i] = nil` in the middle creates holes.
- Numbers are doubles: integers are exact to 2^53. Use `math.floor`/`//` for integer math.
- `tick()` is discouraged: use `os.clock()` for benchmarks, `workspace:GetServerTimeNow()` for
synced time across clients/server, `os.time()`/`DateTime.now()` for wall-clock UTC.
## Types (new type solver)
Roblox's **new type solver** is generally available (Nov 2025) and `Workspace.UseNewLuauTypeSolver`
controls it; `Workspace.LuauTypeCheckMode` sets the default mode. Write code that checks cleanly
under `--!strict`.
```luau
--!strict
export type Item = {
id: string,
name: string,
stack: number,
tags: { string }?, -- optional field
}
type Rarity = "Common" | "Rare" | "Legendary" -- singleton (literal) union
local function totalStack(items: { Item }, filter: ((Item) -> boolean)?): number
local total = 0
for _, item in items do
if filter == nil or filter(item) then
total += item.stack
end
end
return total
end
local function first<T>(list: { T }): T? -- generics
return list[1]
end
local rarity: Rarity = "Rare"
local part = workspace:FindFirstChild("Door") :: BasePart? -- cast with ::
print(totalStack({}, nil), first({ 1, 2 }), rarity, part)
```
- Refine before use: `if part then ... end`, `if typeof(x) == "Instance" and x:IsA("BasePart") then`.
- `typeof(v)` knows Roblox types (`"Vector3"`, `"Instance"`, ...); `type(v)` only knows Lua types.
- Instance types come from `IsA` refinement or `FindFirstChildOfClass`/`FindFirstChildWhichIsA`.
- Avoid `any` except at trust boundaries (remote payloads, decoded JSON) — validate there, then narrow.
Details: generics, type packs, `read`/`write` properties, user-defined type functions, typing
OOP classes, and migrating legacy code to strict: [references/type-system.md](references/type-system.md).
## Modules and require
ModuleScripts run once per environment (server or each client) and return one value, cached for every
later `require`. A module in `ReplicatedStorage` required by both sides runs twice, once per side, with
separate state.
```luau
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Signal = require(ReplicatedStorage.Packages.Signal) -- instance path (always works)
local Config = require("./Config") -- require-by-string: sibling of this script (script.Parent)
local Utils = require("@self/Utils") -- child of this script
local Shared = require("@game/ReplicatedStorage/Shared") -- absolute from game
print(Signal, Config, Utils, Shared)
```
- String paths: `./` = `script.Parent`, `../` = `script.Parent.Parent`, `@self/` = `script`,
`@game/` = `game`. They **do not wait** for instances to exist. Use instance paths with
`WaitForChild` when the module may not have replicated yet.
- Never create circular requires; they deadlock/error. Break cycles with dependency injection or events.
- Don't yield at module top level (no `WaitForChild` chains on things that may never exist, no
DataStore calls): every requirer blocks until the module returns.
### Module / class pattern
```luau
--!strict
local Counter = {}
Counter.__index = Counter
export type Counter = typeof(setmetatable({} :: { value: number, step: number }, Counter))
function Counter.new(step: number?): Counter
return setmetatable({ value = 0, step = step or 1 }, Counter)
end
function Counter.increment(self: Counter): number
self.value += self.step
return self.value
end
return Counter
```
Prefer plain modules of functions + data tables over deep inheritance. Use composition.
## Errors and retries
```luau
local DataStoreService = game:GetService("DataStoreService")
local store = DataStoreService:GetDataStore("PlayerData")
local function retry<T...>(attempts: number, fn: () -> T...): (boolean, T...)
local delaySeconds = 1
for attempt = 1, attempts do
local result = table.pack(pcall(fn))
if result[1] then
return table.unpack(result, 1, result.n)
end
if attempt < attempts then
warn(`attempt {attempt} failed: {result[2]}`)
task.wait(delaySeconds)
delaySeconds *= 2 -- exponential backoff
end
end
return false
end
local ok, data = retry(3, function()
return store:GetAsync("Player_1")
end)
print(ok, data)
```
- `error("message", 2)` blames the caller; `error({ code = "X" })` throws a table for structured errors.
- `xpcall(fn, debug.traceback)` keeps the stack trace.
- Don't wrap everything in `pcall` — only calls that can fail for reasons outside your code.
## Concurrency: the task library
| Call | Behavior |
| --- | --- |
| `task.spawn(fn, ...)` | Run now (until first yield) on a new thread. |
| `task.defer(fn, ...)` | Run at the end of the current resumption cycle. Good for "after this finishes". |
| `task.delay(t, fn, ...)` | Run after `t` seconds. Returns a thread you can `task.cancel`. |
| `task.wait(t?)` | Yield ≥ `t` seconds (one frame if omitted); returns elapsed time. |
| `task.cancel(thread)` | Stop a spawned/delayed thread. |
| `task.desynchronize()` / `task.synchronize()` | Parallel Luau (inside Actors) — see `roblox-performance`. |
- Per-frame work: `RunService.Heartbeat` (after physics), `RunService.PreSimulation`/`PostSimulation`,
`RunService:BindToRenderStep` (client, before render, ordered). Don't `while true do task.wait() end` for
per-frame logic; connect to an event and use its `deltaTime`.
- Write handlers that work under **deferred** signal behavior (`Workspace.SignalBehavior`): handlers run
later in the frame, not inline. New template places use `Deferred`, `Default` will switch to it, and
server authority requires it. Don't rely on handler side effects being visible right after `:Fire()`.
- Never yield inside metamethods or `Changed`-style callbacks that must be synchronous.
## Cleanup (memory leaks are the #1 Roblox bug)
```luau
local Players = game:GetService("Players")
local connections: { RBXScriptConnection } = {}
table.insert(
connections,
Players.PlayerRemoving:Connect(function(player)
print(`{player.Name} left`)
end)
)
local function cleanup()
for _, connection in connections do
connection:Disconnect()
end
table.clear(connections)
end
cleanup()
```
- `Instance:Destroy()` disconnects that instance's connections and locks `Parent`; it does **not**
clear your Lua references — nil out tables that hold players/instances on `PlayerRemoving`.
- Use `:Once()` for one-shot connections. Use a Trove/Janitor/Maid-style helper for groups.
- Keys that are Player/Instance objects in long-lived tables leak unless removed on leave/destroy.
## Buffers and vectors (performance-sensitive data)
```luau
local packet = buffer.create(10)
buffer.writeu16(packet, 0, 513) -- item id
buffer.writef32(packet, 2, 12.5) -- amount
buffer.writei32(packet, 6, -4)
print(buffer.readu16(packet, 0), buffer.readf32(packet, 2), buffer.len(packet))
local v = vector.create(1, 2, 3) -- native 3-float vector (same type as Vector3 at runtime)
print(vector.magnitude(v), vector.normalize(v), vector.dot(v, vector.one))
```
`buffer` is the right tool for compact network payloads, large grids, and binary serialization.
Standard-library quick reference: [references/stdlib-cheatsheet.md](references/stdlib-cheatsheet.md).
## Legacy → current (most common mistakes)
| Don't write | Write instead |
| --- | --- |
| `wait()`, `spawn()`, `delay()` | `task.wait()`, `task.spawn()`, `task.delay()` |
| `game.Workspace`, `game.Players` | `workspace`, `game:GetService("Players")` |
| `Instance.new("Part", parent)` | create, set properties, **then** set `.Parent` last |
| `Humanoid:LoadAnimation()` | `Animator:LoadAnimation()` |
| `Model:SetPrimaryPartCFrame()` | `Model:PivotTo()` / `Model:GetPivot()` |
| `workspace:FindPartOnRay(Ray.new(...))` | `workspace:Raycast(origin, direction, params)` |
| `BodyVelocity`, `BodyPosition`, `BodyGyro` | `LinearVelocity`, `AlignPosition`, `AlignOrientation` |
| `player:LoadCharacter()` | `player:LoadCharacterAsync()` |
| `MarketplaceService:GetProductInfo()` | `:GetProductInfoAsync()` |
| `BadgeService:AwardBadge()` | `:AwardBadgeAsync()` |
| `player:GetRankInGroup()` | `GroupService:GetRolesInGroupAsync()` / `player:IsInGroupAsync()` |
| `TeleportService:Teleport*()` variants | `TeleportService:TeleportAsync()` |
| `Chat` service / `ChatService` modules | `TextChatService` |
| `GuiObject.Draggable` | `UIDragDetector` |
| `table.getn`, `table.foreach`, `getfenv` | `#t`, `for ... in`, nothing |
The full deprecation map lives in the `roblox-code-review` skill.
## Performance idioms (only where it matters)
- Measure first (`debug.profilebegin/profileend`, MicroProfiler, Script Profiler).
- `--!native` at the top of a script, or `@native` on a function, compiles hot numeric code natively.
Use it on math-heavy modules, not everywhere (compile time + memory limits). Type annotations help it.
- Cache `Instance` lookups and service references outside loops; avoid `FindFirstChild` chains per frame.
- Build large strings with `table.concat` or `buffer`, not repeated `..` in loops.
- Reuse `RaycastParams`/`OverlapParams` objects instead of creating them per call.
## Related skills
`roblox-architecture` (project structure, client/server), `roblox-networking` (remotes),
`roblox-performance` (profiling, Parallel Luau), `roblox-code-review` (review checklist, full deprecation map).