Modules and the require System
As your Lua programs grow, you’ll need a way to organize code into reusable pieces. Modules require a loading mechanism, and Lua provides it through the require function. Modules let you split your code across files, share functionality between projects, and keep your codebase maintainable. In this tutorial, you’ll learn how modules work in Lua, how to use require to load them, and best practices for organizing your code.
Prerequisites
You’ll need a Lua interpreter (5.1 or later) and a text editor. Everything in this tutorial works with the standard Lua distribution; no external libraries required.
What are modules?
A module in Lua is simply a file containing Lua code that you can load into your program. Modules help you:
- Encapsulate code; Keep related functions and data together
- Avoid global namespace pollution; Variables stay contained within modules
- Reuse code by importing functionality across different projects
- Maintainability from smaller, focused files that are easier to understand and debug
Every .lua file can serve as a module. When you load it, Lua executes the file and returns whatever value you specify as the module.
Creating your first module
Let’s create a simple module that provides mathematical utilities. Create a file called mathutil.lua:
-- mathutil.lua
local M = {}
function M.add(a, b)
return a + b
end
function M.subtract(a, b)
return a - b
end
function M.square(x)
return x * x
end
return M
The local M = {} line creates a fresh table, functions attach to it with M.func = function()...end, and return M sends that table back to the caller. Using local for the module table prevents accidental global leakage; without it, M would end up in the global namespace and could collide with other variables. When you call require("mathutil"), Lua runs the file, captures whatever it returns, and hands it to you. Now let’s load this module from a separate file:
-- main.lua
local mathutil = require("mathutil")
print(mathutil.add(3, 5)) -- Output: 8
print(mathutil.subtract(10, 4)) -- Output: 6
print(mathutil.square(4)) -- Output: 16
The require function loads the module and returns whatever value the module returns. In this case, it’s a table with functions we can call using dot notation.
How require Works
The require function is Lua’s primary mechanism for loading modules. Here’s what happens when you call require("mathutil"):
- Lua checks if the module is already loaded in
package.loaded - If not, it searches for the module file using
package.path - Once found, Lua executes the file and stores the result in
package.loaded - The result is returned to your code
The key insight is that require caches loaded modules. If you call require multiple times for the same module, Lua returns the cached version without re-executing the file:
local m1 = require("mathutil")
local m2 = require("mathutil")
print(m1 == m2) -- Output: true (same table, cached)
This behavior is efficient but can surprise beginners expecting fresh loads on each call.
Understanding package.path
Lua finds modules by searching directories listed in package.path. This is a colon-separated list of paths (semicolon-separated on Windows). Each path can contain a question mark ? that Lua replaces with the module name:
-- Display the default search path
for path in string.gmatch(package.path, "[^;]+") do
print(path)
end
Typical output includes the current directory, an init.lua variant for directory-based packages, and system-wide Lua installation paths. Each entry describes a pattern Lua tries in order during resolution. The question mark placeholder gets replaced with your module name; so require("mymod") against ./?.lua becomes ./mymod.lua:
./?.lua
./?/init.lua
/usr/local/share/lua/5.4/?.lua
/usr/local/share/lua/5.4/?/init.lua
You can add your own directories by modifying package.path. This is essential when your modules live outside the default search locations; common in projects with a lib/ or vendor/ directory structure. Appending paths means Lua checks your custom directories after the defaults, while prepending gives them priority:
-- Add current directory's lib subfolder to search path
package.path = package.path .. ";./lib/?.lua;/lib/?/init.lua"
This lets you organize modules in subdirectories and still find them with require. For Lua embedded in game engines or other runtime environments where the filesystem is restricted, adjusting package.path is often the first step toward a workable module setup.
Beyond file-based loading, Lua offers another mechanism entirely.
Using package.preload
Sometimes you want to load a module without a file; perhaps it’s generated dynamically or lives in C code. package.preload lets you define a custom loader for specific module names:
-- Register a custom loader for "virtual" modules
package.preload["virtual"] = function()
return {
name = "Virtual Module",
version = "1.0.0"
}
end
local v = require("virtual")
print(v.name) -- Output: Virtual Module
print(v.version) -- Output: 1.0.0
This is useful for embedding modules, mocking during testing, or interfacing with non-file-based resources. The package.preload table takes priority over file-based loading; if a loader exists for a module name, Lua calls it without touching the filesystem.
module vs require
Understanding the evolution of Lua’s module system helps you read older codebases. Lua 5.1 introduced the module function as the standard way to define packages, and you’ll still encounter it in legacy projects such as World of Warcraft addons or embedded scripting environments that pinned to 5.1. Older Lua code often uses the module function to create packages:
-- Old-style module (Lua 5.1 style)
module("mathutil", package.seeall)
function add(a, b)
return a + b
end
This approach:
- Creates a global table with the module name
- Sets up the environment to capture future function definitions
- Is considered legacy in Lua 5.2+
The module() function was deprecated because it silently creates global variables, making dependency chains hard to trace. If two modules both call module("config"), the second silently overwrites the first with no warning. Modern Lua encourages explicit table returns instead. The modern approach uses require with a table return:
-- Modern style (recommended)
local M = {}
-- Define functions in M
return M
Prefer the modern style. It’s cleaner, works better with Lua 5.4, and avoids polluting the global namespace.
Best practices for organizing modules
Follow these patterns to keep your Lua code maintainable:
Return a single table: Always return one table containing your module’s public API. Keeping the API surface in one place means callers know exactly what to expect; no hidden exports scattered across the file:
local M = {}
-- Private function (not returned)
local function internalHelper()
-- ...
end
-- Public function
function M.publicFunction()
-- ...
end
return M
Marking a function as local inside a module creates genuine privacy in Lua; callers outside the file can never access it. There are no public or private keywords, so the module table pattern is your only encapsulation tool. Functions attached to M become the public API; everything else stays hidden.
Use local variables: Declare your module table as local to avoid globals:
local mymodule = {} -- Not: mymodule = {}
Name files descriptively: Match filename to module name. mathutil.lua becomes require("mathutil"). This convention makes your require calls predictable; anyone reading the code can map require("mymod") straight to mymod.lua on disk without guessing. When your project grows beyond a dozen modules, descriptive filenames prevent the “which file defines that function?” scavenger hunt.
Use init.lua for packages: A file named init.lua in a directory lets you treat the directory as a package:
-- mypackage/init.lua
local M = {}
M.version = "1.0"
return M
The init.lua pattern works because package.path includes ?/init.lua entries. When you call require("mypackage"), Lua resolves the path to mypackage/init.lua and loads it. This lets you bundle multiple related modules under one directory; the main init.lua is the entry point that can require sub-modules internally while exposing a clean public API:
local pkg = require("mypackage") -- Loads mypackage/init.lua
Handling Dependencies
As projects grow beyond a handful of modules, dependencies multiply. A config module might feed into a logging module, which feeds into a network module, which feeds into every other module in the system. Managing these relationships explicitly keeps your code predictable. When your modules depend on other modules, manage those dependencies clearly:
-- config.lua
local M = {}
M.defaults = {
host = "localhost",
port = 8080
}
return M
Placing require calls at the top of a file makes every dependency visible immediately; no hunting through the code to figure out what the module needs. Lua loads each required module once and caches the result, so the order of require statements does not affect performance. What matters is avoiding circular chains where two modules each try to load the other during initialization:
-- app.lua
local config = require("config")
local mathutil = require("mathutil")
local settings = config.defaults
settings.timeout = mathutil.square(10)
In larger projects, consider a dependency injection pattern or a module that centralizes configuration. The key principle is making dependency direction one-way: higher-level modules depend on lower-level ones, never the reverse. When you violate this rule, you hit the hardest class of Lua module bugs.
Common Pitfalls
Even experienced Lua developers trip over these module mistakes. Knowing what they look like saves hours of debugging. Watch out for these common mistakes:
Forgetting to return: A module without a return statement returns nil:
-- broken.lua
function hello()
print("hi")
end
-- Missing return!
When a module file executes without hitting a return statement, Lua returns nil to require. The caller receives nil instead of a table, and any attempt to access b.hello() produces the dreaded Lua error “attempt to call a nil value.” A good habit is to always write return M before writing anything else in the module body:
local b = require("broken")
print(b) -- Output: nil
Circular requires: The trickiest module bug involves two files that each require the other. Lua’s caching mechanism means one of them receives an incomplete module; a table that hasn’t finished populating its fields yet. If module A requires B and B requires A, you might get partial tables:
-- a.lua
local b = require("b")
local M = { value = "a" }
M.b = b
return M
The problem here is subtle: when a.lua starts loading, it immediately calls require("b"). Module b then calls require("a"), but Lua hasn’t finished loading a yet. At that point, package.loaded["a"] contains nil because local M = { value = "a" } hasn’t executed yet. This means b gets nil for a, and any field access on it will fail. The fix is to restructure so the circular dependency breaks, or to defer the require call until after module initialization:
-- b.lua
local a = require("a") -- Gets A before it's fully initialized
local M = { value = "b" }
return M
Solve this by restructuring or using lazy loading.
Wrong path: If require("mymod") fails, check that:
- The file exists in one of
package.pathdirectories - The filename matches exactly (case-sensitive on Linux)
- The file has
.luaextension or you’re usinginit.luapattern
Next steps
Now that you understand how Lua modules and require work, the next tutorial in this series covers error handling with pcall and xpcall, an essential skill for writing dependable Lua programs.
Summary
Modules keep code clean. Require loads once and caches. Package.path finds your files. These are the bedrock of every maintainable Lua application. The key points to remember:
- Modules are
.luafiles that return a table of functions - Use
requireto load modules; it caches results inpackage.loaded - Set up
package.pathto tell Lua where to find your modules - Prefer the modern table-return pattern over the legacy
modulefunction - Return a single table, keep functions local, and organize with
init.luafor packages
With these fundamentals, you can structure any Lua project cleanly and scale it as it grows.
See also
- Error handling with pcall and xpcall — error handling with pcall and xpcall
- String and pattern matching basics — string and pattern matching basics
- Reading and writing files in Lua — reading and writing files in Lua