Back to Blog

5 Luau Mistakes Every New Roblox Developer Makes

March 12, 2026

Learning Luau is the first step to building awesome Roblox games. But every new developer runs into the same set of traps. This guide covers the five most common Luau scripting mistakes and shows you exactly how to avoid them. If you are just starting out, bookmark this one.

1. Not Using --!strict Mode

Luau has a built-in type checker that catches bugs before your game even runs. The problem? It is turned off by default. Most new developers never turn it on, so they spend hours debugging errors that the type checker would have caught instantly.

Add this line at the very top of every script you write:

Any Script
--!strict

local Players = game:GetService("Players")

local function getPlayerCoins(player: Player): number
    local leaderstats = player:FindFirstChild("leaderstats")
    if not leaderstats then
        return 0
    end
    local coins = leaderstats:FindFirstChild("Coins")
    if not coins then
        return 0
    end
    return coins.Value
end

With --!strict enabled, Luau will underline type mismatches, missing arguments, and nil access right in the Script Editor. It takes two seconds to add and saves you hours of debugging.

2. Putting Server Code in LocalScripts

This is the number one security mistake beginners make. If you put game logic (like giving coins, saving data, or spawning items) inside a LocalScript, exploiters can manipulate it. LocalScripts run on the player's computer. Anything running there can be modified by cheaters.

The rule is simple: if the code changes game state (data, currency, inventory), it belongs in a ServerScript inside ServerScriptService. LocalScripts handle UI and input only.

Bad: LocalScript giving coins (exploitable)
-- DO NOT DO THIS in a LocalScript!
local player = game.Players.LocalPlayer
player.leaderstats.Coins.Value = player.leaderstats.Coins.Value + 100
Good: ServerScript giving coins (secure)
--!strict
-- ServerScript in ServerScriptService
local Players = game:GetService("Players")

local function awardCoins(player: Player, amount: number)
    local leaderstats = player:FindFirstChild("leaderstats")
    if leaderstats then
        local coins = leaderstats:FindFirstChild("Coins")
        if coins then
            coins.Value += amount
        end
    end
end

3. Not Using pcall for DataStore Operations

DataStore calls can fail at any time. Roblox's DataStore service has rate limits and the servers occasionally hiccup. If you call GetAsync or SetAsync without wrapping them in pcall, your script will error and break.

Bad: No error handling
local DataStoreService = game:GetService("DataStoreService")
local store = DataStoreService:GetDataStore("PlayerData")

-- This will CRASH your script if the call fails
local data = store:GetAsync("Player_123")
Good: Wrapped in pcall with retry
--!strict
local DataStoreService = game:GetService("DataStoreService")
local store = DataStoreService:GetDataStore("PlayerData")

local MAX_RETRIES = 3

local function safeGet(key: string): any?
    for attempt = 1, MAX_RETRIES do
        local success, result = pcall(function()
            return store:GetAsync(key)
        end)
        if success then
            return result
        end
        warn("DataStore get failed (attempt " .. attempt .. "): " .. tostring(result))
        task.wait(1)
    end
    warn("DataStore get failed after " .. MAX_RETRIES .. " attempts")
    return nil
end

Always wrap DataStore calls in pcall. Add a retry loop for extra reliability. Your players will thank you when their save data does not randomly vanish.

4. Ignoring RemoteEvent Validation

RemoteEvents let the client talk to the server. But here is the catch: exploiters can fire RemoteEvents with any data they want. If your server just trusts whatever the client sends, you are wide open to cheating.

Bad: Trusting the client blindly
-- ServerScript
local remote = game.ReplicatedStorage.BuyItem

remote.OnServerEvent:Connect(function(player, itemName, price)
    -- DANGER: An exploiter can send price = 0
    player.leaderstats.Coins.Value -= price
    giveItem(player, itemName)
end)
Good: Server validates everything
--!strict
local remote = game.ReplicatedStorage.BuyItem
local ITEM_PRICES: {[string]: number} = {
    Sword = 100,
    Shield = 75,
    Potion = 25,
}

remote.OnServerEvent:Connect(function(player: Player, itemName: unknown)
    -- Validate the type
    if typeof(itemName) ~= "string" then
        return
    end

    -- Look up the REAL price on the server
    local price = ITEM_PRICES[itemName]
    if not price then
        return
    end

    local coins = player.leaderstats.Coins
    if coins.Value >= price then
        coins.Value -= price
        giveItem(player, itemName)
    end
end)

Never trust data from the client. Always validate types, check against server-side values, and treat every RemoteEvent fire as potentially malicious.

5. Hardcoding Values Instead of Using ModuleScripts

New devs love to sprinkle magic numbers all over their code. The sword does 25 damage in one script, but then you have to find and change that number in five different places. ModuleScripts solve this.

Bad: Magic numbers everywhere
-- Script 1: SwordHandler
humanoid:TakeDamage(25)

-- Script 2: ShopUI
priceLabel.Text = "Sword: 100 coins"

-- Script 3: CombatLog
print(player.Name .. " dealt 25 damage")
Good: Shared config ModuleScript
--!strict
-- ModuleScript in ReplicatedStorage/Config/WeaponStats
local WeaponStats = {
    Sword = {
        damage = 25,
        price = 100,
        cooldown = 0.8,
    },
    Bow = {
        damage = 15,
        price = 150,
        cooldown = 1.2,
    },
}

return WeaponStats
Then use it anywhere:
--!strict
local WeaponStats = require(game.ReplicatedStorage.Config.WeaponStats)

-- Now changing damage is a one-line edit in WeaponStats
humanoid:TakeDamage(WeaponStats.Sword.damage)

Put your game constants in a ModuleScript inside ReplicatedStorage. Every script that needs those values just require() the module. One place to edit, zero chance of mismatched values.

Wrapping Up

These five mistakes trip up almost every new Roblox developer. The good news is they are all easy to fix. Turn on --!strict, keep game logic on the server, wrap DataStore calls in pcall, validate RemoteEvents, and use ModuleScripts for shared config. Do that and you are already ahead of most beginners.

Ready to try BloxCode?

Get AI-powered Roblox development help for free. No credit card required.

Start Building for Free