Modules & Error Handling
Learn Lua's module pattern with require, and handle errors gracefully with pcall, xpcall, error, and assert.
Introduction
As a Lua project grows past a single file, you need a way to organize code across files and handle things going wrong gracefully. Lua solves both with mechanisms you've already met — tables and functions — plus a small, focused set of error-handling functions.
- Lua's module pattern: returning a table from a file to expose its public API.
- How require() loads modules and how package.path controls where it looks.
- How to raise errors with error() and assert(), and catch them safely with pcall().
The Module Pattern
A Lua module is just a regular .lua file that builds a table of its public functions/values and returns it at the end — no special module syntax needed, since tables and functions already do all the work.
-- mathutils.lualocal mathutils = {}
function mathutils.square(x) return x * xend
function mathutils.cube(x) return x * x * xend
return mathutilsrequire and package.path
Use case: require("modulename") loads a file (searching the locations listed in package.path), runs it once, and caches its returned value — calling require() again for the same module returns the cached result instead of re-running the file.
-- main.lualocal mathutils = require("mathutils")
print(mathutils.square(4))print(mathutils.cube(3))Click Run to see what this code prints.
A required module's file body runs exactly once, and anything declared local inside it (but not returned in the table) stays completely private — the exact same encapsulation idea from lesson 8's closures, applied at the file level.
error and assert
Use case: error(message) immediately stops execution and raises an error with the given message. assert(condition, message) is a shortcut — it raises an error with message if condition is falsy, and otherwise does nothing and returns its arguments.
local function divide(a, b) if b == 0 then error("cannot divide by zero") end return a / bend
local function safeDivide(a, b) assert(type(a) == "number", "a must be a number") assert(type(b) == "number", "b must be a number") return divide(a, b)end
print(safeDivide(10, 2))Click Run to see what this code prints.
pcall: Catching Errors
Use case: pcall(function, ...) calls a function in "protected mode" — instead of letting an error crash the whole script, it catches it and returns a boolean success flag plus either the function's normal return values or the error message.
local ok, result = pcall(divide, 10, 0)
if ok then print("Result:", result)else print("Error caught:", result)endClick Run to see what this code prints.
xpcall and Error Handlers
Use case: xpcall(function, handler, ...) works like pcall but runs a custom error-handling function at the moment the error occurs, useful for attaching a stack traceback before the call stack unwinds.
local function handler(err) return "Handled: " .. tostring(err)end
local ok, result = xpcall(divide, handler, 10, 0)print(ok, result)Click Run to see what this code prints.
Common Mistakes
- Forgetting to return the module table at the end of a module file — require() will return nil (or true, in older Lua versions) instead of your API.
- Wrapping every single function call in pcall() defensively — reserve it for operations that can genuinely fail (file I/O, parsing untrusted input), not routine internal logic.
- Ignoring the boolean success flag pcall() returns and only looking at the second return value.
Best Practices
- Keep each module's internal helper functions local, and only expose what callers actually need on the returned table.
- Use assert() for validating function preconditions — it's more concise than a manual if + error().
- Reach for pcall() at clear failure boundaries (file loading, network calls, parsing) rather than scattering it throughout otherwise-trusted code.
Frequently Asked Questions
Conceptually yes — it loads and runs another file and gives you its exported value, though mechanically it's just a regular Lua function call, not special syntax.
Lua caches the result the first time a module is loaded, so a second require() call for the same name returns the cached table instead of re-running the file.
Yes — error() can raise any Lua value (like a table with structured error details), which pcall() will hand back to you exactly as raised.
Key Takeaways
- A Lua module is a regular file that builds and returns a table of its public API.
- require() loads and caches a module, searching package.path for matching files.
- error() raises an error immediately; assert() raises one only if a condition is falsy.
- pcall() (and xpcall() with a custom handler) catch errors instead of letting them crash the whole script.
Summary
You can now organize Lua code across multiple files and handle failures without crashing your whole program. In the final lesson, you'll see how Lua actually gets embedded inside a host application, plus a recap of best practices across everything you've learned.