Appearance
Modules and Require
Written: 2026-10-14
Learning objectives
- You shape a ModuleScript as a local table plus return.
- You explain require caching and shared references.
- You require siblings through their parent instance.
- You read string paths as a filesystem habit, not engine behavior.
- You avoid circular requires.
- You place shared modules in ReplicatedStorage safely.
- You use WaitForChild on the client and init.luau folders for large modules.
Prerequisites
A module is a local table plus a return
A ModuleScript builds a value and returns it. Most modules return a table of functions. Some return a single function or a frozen config table. The return value cannot be nil. The reuse guide in modules shows the canonical shape.
Put this ModuleScript in ReplicatedStorage and name it CoinMath.
luau
--!strict
local CoinMath = {}
function CoinMath.bonus(base: number, mult: number): number
return math.floor(base * mult)
end
function CoinMath.fee(amount: number): number
return math.floor(amount * 0.05)
end
return CoinMathThe module creates one local table, attaches two functions, and returns the table. No other code runs until another script requires it. Callers see exactly those two functions.
Put this ModuleScript in ReplicatedStorage and name it GameConfig.
luau
--!strict
local GameConfig = {
maxPlayers = 8,
roundSeconds = 120,
}
table.freeze(GameConfig)
return GameConfigThe module builds a small dictionary and freezes it so gameplay code cannot edit tuning by accident. The frozen table travels to every requirer. Config modules like this beat dozens of scattered value objects. They also store tables and functions, which attributes cannot.
Require runs once and shares the result
require executes the ModuleScript on first call and caches the returned value. Later calls return the same reference without rerunning the module. If the module returns a table, every requirer on the same side of the client server boundary shares one table. Mutations show everywhere.
Put this ModuleScript in ReplicatedStorage and name it Counter.
luau
--!strict
local Counter = {n = 0}
function Counter.bump(): number
Counter.n += 1
return Counter.n
end
return CounterPut this Script in ServerScriptService.
luau
--!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local A = require(ReplicatedStorage:WaitForChild("Counter"))
local B = require(ReplicatedStorage:WaitForChild("Counter"))
print(A.bump())
print(B.bump())
print(A.n)txt
1
2
2Both locals point at one cached table. The first bump prints 1. The second bump prints 2. The final read shows 2 through either name. This sharing suits services, registries, and caches. Per-player state needs a constructor instead of a shared table. Constructors appear in Metatables and OOP.
Client and server keep separate caches. A module placed in ReplicatedStorage runs once per side. Each side gets its own reference. Never assume client writes reach the server through a shared module. Replication needs remotes, covered later in the course.
Relative requires stay close
In Studio you require instances, not strings. The engine's require takes the ModuleScript object itself, so a sibling require goes through the shared parent. A folder named Shop with two ModuleScripts named Cart and Prices is one example. A Script inside Cart that needs its sibling requires through the folder.
Put this ModuleScript in ReplicatedStorage inside a folder named Shop and name it Prices.
luau
--!strict
local Prices = {apple = 5}
return PricesPut this ModuleScript in ReplicatedStorage inside the same folder named Shop and name it Cart.
luau
--!strict
local Prices = require(script.Parent:WaitForChild("Prices"))
local Cart = {}
function Cart.appleCost(n: number): number
return Prices.apple * n
end
return Cartscript.Parent is the Shop folder both modules share, so moving the whole folder keeps the link valid. Prefer parent-relative requires for siblings and children. Use full instance chains only when the target lives far away.
String paths like ./Prices, ../, @self/, and configured @alias/ prefixes belong to filesystem workflows, not the engine. In a Rojo project the toolchain resolves them relative to the requiring file, with @self reserved for the current module's children and other @name/ prefixes defined in the project's require configuration. You write them in files synced by Rojo, never in Studio's Explorer tree. The alias and resolution rules live in the require RFCs, and the project layout lives in Rojo Project Structure.
Circular requires error at runtime
Two modules must not require each other. When A requires B and B requires A during startup, Luau stops with Requested module was required recursively. This is a runtime error, not a type error. The reuse guide in modules warns against the cycle.
Break cycles by extracting shared data into a third module. Both rivals require the shared leaf. Neither requires the other. Inverting the dependency is another option: pass the needed function as a parameter instead of requiring it at the top.
A safe trio looks like this. Put these three ModuleScripts in ReplicatedStorage.
luau
--!strict
local Shared = {tax = 2}
return Sharedluau
--!strict
local Shared = require(script.Parent:WaitForChild("Shared"))
local A = {}
function A.total(n: number): number
return n + Shared.tax
end
return AThat second block requires only the leaf. A third module for B would do the same. No cycle forms. Check new modules for back edges during review. Cycles often hide behind a helper that requires its caller.
ReplicatedStorage shares code across the boundary
ReplicatedStorage is the standard home for modules used by both client and server. Both sides can see it. Both sides require from it. Each side still runs its own copy, as noted above. Place pure logic, configs, and validators here. The syntax tour covers language shapes, while modules covers placement.
Server-only modules live in ServerScriptService. They hold secrets, DataStore code, and authority checks. The client cannot see that container, so secrets stay hidden. Client-only helpers live under StarterPlayerScripts or ReplicatedFirst when every client needs them early. Choose the container by asking who may run the code.
Keep modules focused. One domain per module. A pricing module does not send network messages. A network module does not balance the economy. Small modules load fast, test cleanly, and break visibly.
WaitForChild keeps client requires safe
Replication order is not guaranteed on the client. A LocalScript can run before ReplicatedStorage finishes delivering a module. Direct indexing can therefore see nil on some joins. WaitForChild pauses until the child exists, then returns it. Use it for every client-side require from a replicated container.
Put this LocalScript in StarterPlayerScripts.
luau
--!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local CoinMath = require(ReplicatedStorage:WaitForChild("CoinMath"))
print(CoinMath.bonus(100, 1.2))txt
120The client waits for CoinMath, requires it, and calls bonus. The floor of 120 stays 120. On the server you can often require the instance directly because the server builds the tree, but WaitForChild remains harmless when the target may load late. Standardize on it for replicated paths.
Never busy-wait with a while loop for modules. WaitForChild already yields efficiently with a timeout. Set an explicit timeout only when a missing module should fail fast during development.
init.luau folders organize large modules
Some projects split a big module across a folder with an init.luau entry file, per the Rojo sync details and the abstract module paths RFC. The folder acts as the module. The entry file returns the public table. Sibling files inside the folder hold private pieces. Requiring the folder loads the entry file.
Treat this as a filesystem organization habit, not engine behavior. Studio's ModuleScript tree and Rojo-style folders differ in details. Adopt the folder split only when one file grows past easy review, roughly a few hundred lines. Small modules stay single files. Link folder children with relative requires so the folder moves as a unit.
A typical split keeps three parts. The entry builds the public table. A math file holds pure helpers. A data file holds frozen tuning. Each file returns its own table. The entry requires its children and re-exports one surface. Callers require only the entry. Avoid deep require chains because each hop adds startup cost and cycle risk.
Common mistakes
Wrong: you forget the return.
luau
--!strict
local Tools = {}
function Tools.wipe(list: { string }): { string }
table.clear(list)
return list
endWithout a final return Tools, requirers get nil and the next call errors on indexing nil. Fix it by ending every ModuleScript with its public value.
luau
--!strict
local Tools = {}
function Tools.wipe(list: { string }): { string }
table.clear(list)
return list
end
return ToolsWrong: you index a replicated module directly on the client.
luau
--!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local CoinMath = require(ReplicatedStorage.CoinMath)This works in Play Solo but can miss on real joins when replication lags. Fix it with WaitForChild.
luau
--!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local CoinMath = require(ReplicatedStorage:WaitForChild("CoinMath"))
print(CoinMath.bonus(10, 2))txt
20Wrong: you build a require cycle.
luau
--!strict
local B = require(script.Parent:WaitForChild("B"))
local A = {}
return AWhen B requires A back during startup, Luau raises Requested module was required recursively. Fix it by extracting shared values into a leaf module that both sides require.
Wrong: you store per-player state in a shared module table.
luau
--!strict
local State = {coins = 0}
return StateEvery server script shares one coins field. One player overwrites another. Fix it by storing per-player rows keyed by UserId, or by returning a constructor that builds a fresh table per caller.
Exercises
- Write a frozen config module and require it from a server Script. Print two fields.
- Prove require caching. Require one counter module twice, bump through both names, and show the shared count.
- Write a relative sibling require with
script.Parent. Move the pair as a folder and confirm the link still reads correctly. - Break a cycle. Create two modules that both need one constant, extract a Shared leaf, and require it from both.
Solution 1
Put this ModuleScript in ReplicatedStorage and name it Rates.
luau
--!strict
local Rates = {xp = 2, coins = 5}
table.freeze(Rates)
return RatesPut this Script in ServerScriptService.
luau
--!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Rates = require(ReplicatedStorage:WaitForChild("Rates"))
print(Rates.xp)
print(Rates.coins)txt
2
5One frozen table serves every requirer. No caller can retune the game by accident.
Solution 2
Put this ModuleScript in ReplicatedStorage and name it Hits.
luau
--!strict
local Hits = {n = 0}
function Hits.add(): number
Hits.n += 1
return Hits.n
end
return HitsPut this Script in ServerScriptService.
luau
--!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local A = require(ReplicatedStorage:WaitForChild("Hits"))
local B = require(ReplicatedStorage:WaitForChild("Hits"))
print(A.add())
print(B.add())txt
1
2Two names share one cached table, so the count climbs across both calls.
Solution 3
Put this ModuleScript in ReplicatedStorage inside a folder named Lib and name it Base.
luau
--!strict
local Base = {v = 7}
return BasePut this ModuleScript in ReplicatedStorage inside the same folder named Lib and name it Top.
luau
--!strict
local Base = require(script.Parent:WaitForChild("Base"))
local Top = {}
function Top.double(): number
return Base.v * 2
end
return TopPut this Script in ServerScriptService.
luau
--!strict
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Top = require(ReplicatedStorage:WaitForChild("Lib"):WaitForChild("Top"))
print(Top.double())txt
14The ./Base link survives folder moves because it names a sibling relationship, not a full path.
Solution 4
Put this ModuleScript in ReplicatedStorage and name it Shared.
luau
--!strict
local Shared = {bonus = 3}
return SharedPut these two ModuleScripts in ReplicatedStorage and name them TeamA and TeamB.
luau
--!strict
local Shared = require(script.Parent:WaitForChild("Shared"))
local TeamA = {}
function TeamA.score(n: number): number
return n + Shared.bonus
end
return TeamAluau
--!strict
local Shared = require(script.Parent:WaitForChild("Shared"))
local TeamB = {}
function TeamB.score(n: number): number
return n + Shared.bonus
end
return TeamBBoth rivals require the leaf. Neither requires the other, so no recursive-require error can fire.
Summary
- Modules return one value, usually a table. Require caches that value per side.
- Relative paths keep folders portable. Cycles raise a recursive-require error.
- ReplicatedStorage hosts shared code. Clients require it with WaitForChild.
- Split large modules into folders only when size demands it.
- Next: Metatables and OOP.