Modules, require and package.path
Write modules that return a table, understand how require caches and searches, and avoid circular and global-leaking module patterns.
Writing and requiring a module
-- file: geometry.lua
local M = {} -- the conventional module table
local function square(x) return x * x end -- private, stays local
function M.hypot(a, b)
return math.sqrt(square(a) + square(b))
end
M.pi = math.pi
M.VERSION = "1.2.0"
return M -- return exactly one value: the module
-- file: main.lua
local geometry = require("geometry") -- searches on package.path
print(geometry.hypot(3, 4)) --> 5
print(geometry.VERSION)
-- require returns the SAME table every time: modules are singletons
local again = require("geometry")
print(again == geometry) --> true
-- a dotted name maps to a directory path with the remaining dots intact
local text = require("util.text") -- loads util/text.lua
local deep = require("a.b.c") -- loads a/b/c.lua
-- a submodule of a library looks like a field after the fact
-- require("mylib") sets package.loaded["mylib"], not nested tables- Return the module table, and only one value. A module that returns nothing makes every caller use a global, which reintroduces the problem modules exist to solve.
- Keep helpers
local. A module that leaks names into the global table collides with every other module in the process. - Do not access a module's own table through the module name from inside itself. Use the local
M, which is available beforerequirefinishes. requirecaches the result inpackage.loaded. A second call returns the cached value without re-running the file.
package.path, searchers and loaders
-- inspect the current search path
print(package.path)
-- ./?.lua;./?/init.lua;/usr/share/lua/5.4/?.lua;/usr/share/lua/5.4/?/init.lua
-- add a project directory at the front so local modules win
package.path = "./src/?.lua;./src/?/init.lua;" .. package.path
-- a module that replaces itself with a function or a table with state
-- file: app.lua
local M = {}
function M.configure(opts) M.opts = opts end
function M.name() return M.opts and M.opts.name or "unnamed" end
return M
-- package.loaded: preload a value to skip the search entirely
package.loaded["config"] = { env = "test" }
local config = require("config") -- returns the preloaded table
print(config.env) --> test
-- force a reload in a test or a hot-reload loop
package.loaded["config"] = nil
require("config") -- re-runs the file
-- searchers: how require actually finds things, in order
for i, searcher in ipairs(package.searchers or package.loaders) do
print(i, type(searcher))
end
-- 1 preload 2 Lua file on package.path 3 C library on package.cpath 4 all-in-one
-- add your own searcher, for example for a virtual filesystem
table.insert(package.searchers, function(name)
if name == "generated" then
return function() return { value = 42 } end -- a loader function
end
end)
print(require("generated").value) --> 42| Item | Meaning | Where |
|---|---|---|
? in package.path | The module name with dots replaced by slashes | Search template |
package.loaded | The cache of loaded modules | Indexed by module name |
package.preload | Loaders registered by name from C | Checked by the first searcher |
package.searchers | The ordered list of finders | 5.2+; called loaders in 5.1 |
package.cpath | Where native libraries are found | For modules written in C |
A searcher returns either a loader function plus the path it used, or a string explaining why that searcher failed. That string is what makes the final error message list every path that was tried.
Circular dependencies and structure
-- circular requires: A needs B and B needs A
-- file: a.lua
local B = require("b") -- b.lua runs, requires a, and gets a PARTIAL table
local M = {}
function M.run() return B.helper() end
return M
-- file: b.lua
local A = require("a") -- receives the incomplete a table, whose run is not set yet
local M = {}
function M.helper() return A.run end -- capture lazily: do not call at load time
return M
-- the rule: at module load time, only refer to the other module's table.
-- Call its functions later, from your own functions, never at the top level.
-- the cleaner fix: move the shared piece into a third module both can depend on
-- shared.lua -- types and pure helpers
-- a.lua requires shared, b.lua requires shared, neither requires the other- A circular require is not an error in Lua; it silently hands over the partially initialised module. The failure appears later, as an attempt to call a nil field.
- Design modules as a dependency graph with no cycles and one direction. If two modules need each other, the shared part belongs in a third one.
- If a cycle is unavoidable, defer the reference: a local function that calls
requireon first use, or a setter the caller invokes after both modules finish loading. - Avoid module state that depends on load order. A module that reads a global configured by another module is order-dependent and hard to test.
- Use a per-module local for
requirewhen you call it often:local insert = table.insertat the top of the file.
⚠️
Never call
require with a name built from user input without validating it. A crafted name such as os or io pulls a standard library into your sandbox, and on a path-based searcher a name containing .. can escape the module directory.FAQ
Why does require say module not found?
The name did not match
package.path after the dot-to-slash substitution. Print package.path, confirm the file name and the case, and remember that a leading ./ template is relative to the current working directory, not to the script.How do I reload a module during development?
Set
package.loaded["name"] = nil and require it again. Any module that captured the old table still holds the old version, so a true hot reload needs to clear dependents as well.Related
Error handling: error, pcall and xpcall Environments, sandboxing and bytecode
Last refreshed 2026-09-18.