What Is Luau and Why Does Roblox Use It?

Luau is the scripting language that powers every Roblox experience. It started as a fork of Lua 5.1 but has grown into its own language with strict type annotations, a faster runtime, and safety features built specifically for the Roblox engine. Roblox chose Lua originally because it is lightweight, easy to embed inside a game engine, and gentle on beginners. Over time, the Roblox engineering team added so many improvements — type checking, generalized iteration, compound assignment operators like +=, and a custom garbage collector — that they renamed the language to Luau to distinguish it from standard Lua.

If you have ever written a line of code in Roblox Studio, you have written Luau. Every Script, LocalScript, and ModuleScript in the Explorer panel runs Luau under the hood. Understanding how the language works is the single most important skill you can build as a Roblox developer, because literally every gameplay mechanic — from opening a shop GUI to dealing damage — is driven by scripts.

Variables, Types, and Scope

A variable is a named container that holds a value. In Luau you create one with the local keyword. You should almost always use local because it limits the variable's visibility to the block where it was declared, which prevents accidental name collisions across your codebase.

-- Declaring variables
local playerName = "SpawnBlox"       -- string
local health = 100                   -- number
local isAlive = true                 -- boolean
local inventory = {}                 -- table (empty)
local nothing = nil                  -- nil means "no value"

-- Luau supports type annotations (optional but recommended)
local speed: number = 16
local tag: string = "VIP"

Global variables — those declared without local — are stored in the environment table and can be read from any script in the same VM context. That sounds convenient, but in practice it causes hard-to-trace bugs. A global in one script can silently overwrite a global in another script if they share the same name. The community rule of thumb: always use local variables. If you need to share data between scripts, use ModuleScripts or ValueObjects in the DataModel instead of globals.

Scope Basics

A variable's scope is the region of code where it exists. In Luau, scopes are created by do...end, if...then...end, for...do...end, and function bodies. Once you leave a scope, any local variables declared inside it are gone.

local greeting = "Hello"

if true then
    local secret = "hidden"
    print(greeting) -- works, greeting is in an outer scope
end

print(secret) -- nil! secret only existed inside the if block

Functions

Functions are reusable blocks of logic. You define them with the function keyword, pass in parameters, and optionally return a result. In Roblox development, you will write functions constantly — for event handlers, utility calculations, and organizing your code into readable chunks.

local function calculateDamage(baseDamage: number, multiplier: number): number
    return baseDamage * multiplier
end

local finalDamage = calculateDamage(25, 1.5)
print(finalDamage) -- 37.5

-- A practical Roblox example: healing a humanoid
local function healPlayer(player: Player, amount: number)
    local character = player.Character
    if not character then return end

    local humanoid = character:FindFirstChildOfClass("Humanoid")
    if not humanoid then return end

    humanoid.Health = math.min(humanoid.Health + amount, humanoid.MaxHealth)
end

Notice the guard clauses at the top of healPlayer. Defensive checks like these are essential in Roblox because characters can be nil — for example, when a player is respawning. Always verify that an object exists before you index into it.

Tables — The Most Important Data Structure

Tables are the only compound data structure in Luau, and they do the job of arrays, dictionaries, sets, and objects all at once. An array-style table uses sequential integer keys starting at 1. A dictionary-style table uses string keys. You can mix both in the same table, but keeping them separate makes your code clearer.

-- Array-style
local fruits = {"Apple", "Banana", "Cherry"}
print(fruits[1]) -- Apple (Luau arrays start at 1, not 0)

-- Dictionary-style
local playerData = {
    coins = 500,
    level = 12,
    inventory = {"Sword", "Shield"},
}

print(playerData.coins) -- 500

-- Iterating
for index, fruit in fruits do
    print(index, fruit)
end

for key, value in playerData do
    print(key, value)
end

Luau's generalized iteration (the for key, value in table do syntax without calling pairs or ipairs) was one of the first features that separated it from vanilla Lua. You can still use ipairs if you want to iterate only over the array portion in order, but for most cases the generalized form is cleaner.

Tip: Tables are passed by reference. If you pass a table to a function and that function modifies it, the original table changes too. If you need an independent copy, you have to write a clone function or use table.clone().

Client vs Server: The Security Model

This is where many new Roblox developers trip up. Every experience runs on two separate environments simultaneously:

  • Server — a single authoritative machine hosted by Roblox. Script objects (the ones in ServerScriptService or ServerStorage) run here. The server is trusted. It owns the DataStores, manages game state, and has the final say on what happens.
  • Client — each player's device. LocalScript objects run here. The client handles input, camera control, UI, and local visual effects. The client is not trusted. Any exploiter can modify what happens on their own client.

The golden rule is: never trust the client. If a client says "I dealt 9999 damage," the server must validate that claim before applying it. If you let the client directly set values in the DataModel that the server relies on, exploiters will ruin your game within hours of launch. Keep all important game logic — damage, currency, inventory changes — on the server side.

RemoteEvents and RemoteFunctions

Because the client and server are separate environments, they need a communication bridge. Roblox provides two primary objects for this: RemoteEvent for one-way messages and RemoteFunction for request-response calls. You place these inside ReplicatedStorage so both sides can access them.

-- RemoteEvent example
-- Place a RemoteEvent called "DamageRequest" in ReplicatedStorage

-- CLIENT (LocalScript in StarterPlayerScripts)
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local damageEvent = ReplicatedStorage:WaitForChild("DamageRequest")

-- Player clicks to attack
local function onAttack(targetId: number)
    damageEvent:FireServer(targetId)
end

-- SERVER (Script in ServerScriptService)
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local damageEvent = ReplicatedStorage:WaitForChild("DamageRequest")

damageEvent.OnServerEvent:Connect(function(player, targetId)
    -- Validate: is the target real? Is the player close enough?
    local target = workspace:FindFirstChild(tostring(targetId))
    if not target then return end

    local distance = (player.Character.HumanoidRootPart.Position - target.Position).Magnitude
    if distance > 10 then return end -- too far away, reject

    -- Apply damage on the server where it's trusted
    local humanoid = target:FindFirstChildOfClass("Humanoid")
    if humanoid then
        humanoid:TakeDamage(25)
    end
end)

Notice how the server checks distance before applying damage. That single validation step blocks a whole class of exploits where a client claims to hit something across the entire map. Always validate every remote call on the server. Check distances, cooldowns, whether the player actually owns an item, and any other condition that matters.

Warning: Avoid RemoteFunction when the server calls the client, because if the client never responds (or disconnects), the server thread will yield forever. Use RemoteEvent for server-to-client communication instead.

ModuleScripts — Organizing Your Code

A ModuleScript is a script that returns a single value — usually a table of functions. Other scripts import it with require(). This is how you share code without copy-pasting it across dozens of scripts. ModuleScripts can live in ReplicatedStorage (shared between client and server), ServerStorage (server only), or StarterPlayerScripts (client only).

-- ModuleScript in ReplicatedStorage called "MathUtils"
local MathUtils = {}

function MathUtils.clamp(value: number, min: number, max: number): number
    return math.max(min, math.min(max, value))
end

function MathUtils.lerp(a: number, b: number, t: number): number
    return a + (b - a) * t
end

return MathUtils

-- Using it from another script
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local MathUtils = require(ReplicatedStorage:WaitForChild("MathUtils"))

local clamped = MathUtils.clamp(150, 0, 100) -- 100
local mid = MathUtils.lerp(0, 10, 0.5)       -- 5

ModuleScripts are cached after the first require(). If three different scripts all require the same module, the module's code runs only once and all three receive the same returned table. This makes modules perfect for shared state — but be careful with that shared state on the client, because two LocalScripts editing the same module table can create race conditions.

Error Handling with pcall

pcall stands for "protected call." It runs a function in a protected context so that if the function throws an error, your script keeps running instead of stopping entirely. This is critical for any operation that can fail at runtime — DataStore calls, HTTP requests, or anything that depends on user input.

local DataStoreService = game:GetService("DataStoreService")
local playerStore = DataStoreService:GetDataStore("PlayerData")

local function loadPlayerData(userId: number)
    local success, result = pcall(function()
        return playerStore:GetAsync("Player_" .. userId)
    end)

    if success then
        print("Loaded data:", result)
        return result
    else
        warn("Failed to load data:", result)
        return nil
    end
end

Without pcall, a single DataStore failure would crash your script and potentially break the entire game session for a player. With it, you get a clean boolean telling you whether the call succeeded, plus the return value or error message. Always wrap DataStore operations, HttpService requests, and MarketplaceService calls in pcall.

Common Beginner Mistakes and How to Avoid Them

After reviewing thousands of forum posts and debugging sessions, these are the patterns that trip up new developers most often:

  • Using global variables by accident. If you forget local, the variable becomes global and can collide with other scripts. Always write local. Enable the Luau linter in Studio settings — it will flag globals with a warning.
  • Not using WaitForChild. On the client, objects may not have replicated yet when your script runs. Use WaitForChild("ObjectName") instead of indexing directly with the dot operator. On the server, children are generally available immediately, but WaitForChild is still safer when scripts run at startup.
  • Trusting the client. Never let a LocalScript set a player's health, give them currency, or modify the game state directly. Always route changes through a RemoteEvent and validate on the server.
  • Connecting events without disconnecting. If you connect to an event inside a loop or a respawn handler without disconnecting the old connection, you end up with dozens of duplicate listeners. Store the connection in a variable and call :Disconnect() when you no longer need it.
  • Ignoring pcall for DataStores. DataStore operations can fail due to rate limits, network issues, or Roblox outages. A single unprotected GetAsync call can kill your save system. Always wrap them.
  • Polling with while loops instead of using events. Instead of writing while true do ... task.wait(1) end to check if something changed, connect to the appropriate event. Roblox provides .Changed, GetPropertyChangedSignal(), and dozens of service-level events. Use them — they are more responsive and lighter on performance.

Where to Go From Here

Once you are comfortable with variables, functions, tables, and the client-server split, you have the foundation for building real experiences. The next concepts to explore are OOP patterns with metatables, Promise libraries for cleaner async code, CollectionService tags for component-based architecture, and ProfileService or similar DataStore wrappers for reliable player data persistence. Roblox's official documentation at create.roblox.com is thorough and well-maintained — use it as your reference alongside community resources on the DevForum.

The best way to learn scripting is to build something. Pick a small project — a coin collection game, an obby with checkpoints, or a simple tycoon — and work through every problem that comes up. Each bug you fix teaches you more than any tutorial ever will.