Skip to content
deltascripts.
Blog

Luau Functions Explained: Parameters, Return Values and Callbacks

Follow a small progress counter from its inputs to its return values, then connect a callback without accidentally calling it early.

By deltascriptsco-admin Published 5 min read
Blocky character arranging input blocks through a reusable function and receiving a result

A function gives a piece of your script a job and a name. For a progress counter, that job might be “work out how many tasks are left.” Put that calculation in one place and a label, a menu and a test can all ask the same question without copying the arithmetic.

The examples below belong in your own Roblox Studio project. Start with a blank Script in ServerScriptService and open Output. Each code block is a separate experiment; replace the previous block when trying the next one. The comments give the expected output for each call.

Give the function one clear job

local function tasksRemaining(total, completed)
    return math.max(total - completed, 0)
end

local remaining = tasksRemaining(8, 3)
print(remaining) -- Expected: 5
print(tasksRemaining(8, 10)) -- Expected: 0

total and completed are parameters: names used inside the function. The values 8 and 3 are arguments supplied by this particular call. Their order matters. The first number becomes total; the second becomes completed.

The rule in this example is a design choice: extra completions never produce a negative number of remaining tasks. That is why the function uses math.max. If your game treats an over-count as an error, change that rule deliberately instead of silently copying it into every caller.

A definition does not perform the calculation on its own. The call tasksRemaining(8, 3) does. The parentheses are the useful visual clue when you read a long script.

Return a value instead of only printing it

print writes a diagnostic message. return hands a value back to the caller and finishes the current function call. Our calculation returns a number, so another function can format it, compare it with zero or show it in a label. It does not need to scrape a message from Output.

Keep that separation when the script grows. A function named tasksRemaining should not also find a GUI, play a sound and award an item. Those actions make it harder to check whether the calculation itself is correct. A caller can decide what to do with the answer.

Return two related answers when that helps

local function getProgress(total: number, completed: number): (number, number)
    assert(total > 0, "total must be greater than zero")

    local bounded = math.clamp(completed, 0, total)
    local remaining = total - bounded
    local fraction = bounded / total
    return remaining, fraction
end

local remaining, fraction = getProgress(8, 3)
print(remaining) -- Expected: 5
print(fraction) -- Expected: 0.375

This version returns a count and a fraction. The caller receives them in the same order. The fraction is suitable for the width of a progress bar; multiply it by 100 when you want a percentage. It is not already “37.5 percent” in the stored value.

The example assumes finite numeric inputs from your own code. Type annotations document that contract and help Studio report mismatches while editing. They do not turn arbitrary network input into trusted numbers. The assert catches a zero or negative total because dividing by that total would not describe a useful progress bar.

Try three small cases on paper before changing the UI: zero of eight completed, eight of eight, and ten of eight. Under the chosen clamping rule, their fractions are 0, 1 and 1. If those answers match your intended game rule, the display code can use the result.

Keep temporary names inside the function

The local variable bounded exists inside getProgress. Code outside the function cannot read that local by name. Return the information a caller needs instead of turning every temporary calculation into a shared variable.

A local function can also read locals declared in a surrounding scope. That is convenient for configuration, but it creates a dependency that may be easy to miss. Passing the important values as parameters usually makes a small calculation easier to reuse and inspect.

Pass the function when an event needs a callback

local changed = Instance.new("BindableEvent")

local function reportProgress(completed)
    print("Completed tasks:", completed)
end

local connection = changed.Event:Connect(reportProgress)
changed:Fire(3)

-- Keep the listener connected while this short demo is in use.

Connect(reportProgress) passes the function for the event to call later. Writing Connect(reportProgress()) would call it immediately and pass its return value instead. Since this function returns no useful value, that is the wrong thing to give Connect.

When an event listener is no longer needed in a longer-running system, disconnect its connection and destroy the temporary event. Keep the listener connected while waiting for events; cleaning it up immediately after subscribing would defeat the example.

A BindableEvent keeps this demonstration on one side of the game. It is not a way to send data from a player’s client to the server. That boundary uses different objects and needs its own checks.

Read a function call with four questions

When you encounter a function in an unfamiliar script, find its definition and ask: what inputs does it expect, what does it return, what shared state does it read, and what does it change? A name such as refresh answers none of those questions by itself. Follow the body until the job is clear.

For a longer example of tracing branches and values, continue with reading a Luau script without running it. If the next step is sharing your function between scripts, the Script, LocalScript and ModuleScript guide explains where that shared code belongs.

References

Comments 0

No approved comments yet. Start the discussion.

Every comment is reviewed by a moderator before publication. Enter only a nickname and your comment; no email or account is needed.

Privacy policy