本文へスキップ
Conditionals and early returns

Conditionals and early returns

Guard clauses that keep functions flat, returning the expression instead of true or false, the or default trap, and ordering checks so the cheap one runs first.

このドキュメントは今のところ英語です。

Last updated

Most unreadable Lua is not complicated logic. It is simple logic wrapped in four levels of if. These are the four changes that fix nearly all of it.

Guard clauses

Handle the reasons to stop first, one per line, then write the real work at the left margin:

server.lua (nested)
RegisterNetEvent('shop:server:buy', function(item, amount)
    local src = source
    local player = GetPlayer(src)
    if player then
        if Items[item] then
            if type(amount) == 'number' and amount > 0 then
                if player.money >= Items[item].price * amount then
                    giveItem(src, item, amount)
                    takeMoney(src, Items[item].price * amount)
                else
                    notify(src, 'Not enough money')
                end
            end
        end
    end
end)
server.lua (guarded)
RegisterNetEvent('shop:server:buy', function(item, amount)
    local src = source
    local player = GetPlayer(src)
    if not player then return end

    local def = Items[item]
    if not def then return end
    if type(amount) ~= 'number' or amount < 1 or amount > 100 then return end

    local cost = def.price * amount
    if player.money < cost then
        return notify(src, 'Not enough money')
    end

    takeMoney(src, cost)
    giveItem(src, item, amount)
end)

Same logic, no nesting, and every precondition is visible in the first few lines. When you add a fifth check you add a line instead of another level.

Two practical notes. Do the cheap checks first so the expensive ones never run on bad input: a type() check before a database call, not after. And do the checks in dependency order, so nothing reads a value that an earlier check has not confirmed yet.

Return the expression

If both branches return a boolean, the condition already is the answer:

Lua
-- Noise
if role == 'police' or role == 'ems' then
    return true
else
    return false
end

-- The same thing
return role == 'police' or role == 'ems'

The same goes for if x then y = true else y = false end. Assign the comparison.

Default values with or

Lua’s or returns the first value that is not nil or false, which makes defaults short:

Lua
local function greet(name)
    return 'Hello, ' .. (name or 'stranger')
end

Warning

This trap catches everyone once. or treats false exactly like nil, so a default for a boolean option silently ignores the caller.

Lua
-- Broken: passing false gives you true
local function setup(opts)
    local enabled = opts.enabled or true    -- always true
end

-- Correct
local function setup(opts)
    local enabled = opts.enabled
    if enabled == nil then enabled = true end
end

The same applies to numbers when 0 is meaningful in your code. 0 is truthy in Lua, so count or 10 is safe, but value ~= nil is what you want whenever false is a legitimate value.

The related idiom cond and a or b is not a ternary operator. It breaks when a is false or nil, because the and fails and you get b:

Lua
local label = isDead and false or 'alive'   -- always 'alive'

Use it for values you know are never false or nil, and write a real if otherwise.

Prefer the positive form

not costs the reader a mental flip, and two of them cost a re-read:

Lua
-- Backwards
if not isOnDuty then
    hideBlip()
else
    showBlip()
end

-- Forwards
if isOnDuty then
    showBlip()
else
    hideBlip()
end

Name variables positively for the same reason: isReady reads better than isNotReady, and if not isNotReady is how you get bugs.

The exception is a guard clause, which is nearly always negative on purpose (if not player then return end). That is fine: there is no else to flip, and the negative form is the whole point.

A lookup instead of a chain

Once an if / elseif chain is comparing the same variable against five constants, a table says it better and does not get longer with each new case:

Lua
-- Grows a branch per item
if item == 'water' then
    restoreThirst(20)
elseif item == 'cola' then
    restoreThirst(35)
elseif item == 'coffee' then
    restoreThirst(15)
end

-- Grows a line per item, and the data can move to a config file
local DRINKS <const> = { water = 20, cola = 35, coffee = 15 }

local thirst = DRINKS[item]
if thirst then restoreThirst(thirst) end

The table version is also a single hash lookup instead of up to five comparisons, which matters if this runs per frame or per item in a loop.

Next: Tables and loops.