Lua is a lightweight, high-level, multi-paradigm programming language designed primarily for embedded use in applications. It is cross-platform, as the interpreter of compiled bytecode is written in ANSI C.
Use this file to discover all available pages before exploring further.
Lua Anti-Patterns Overview
Lua, despite being a lightweight and flexible language, has several common anti-patterns that can lead to performance issues, maintainability problems, and bugs. Here are the most important anti-patterns to avoid when writing Lua code.
Global Variables by Default
-- Anti-pattern: Implicit global variablesfunction process_data() result = 42 -- Implicitly global! return resultend-- Better approach: Use local variablesfunction process_data() local result = 42 -- Explicitly local return resultend
Always use the local keyword when declaring variables. In Lua, variables are global by default, which can lead to unexpected behavior and name collisions.
Not Using Modules
-- Anti-pattern: Global functions and variablesfunction calculate_area(radius) return math.pi * radius * radiusendfunction calculate_circumference(radius) return 2 * math.pi * radiusend-- Better approach: Use moduleslocal Circle = {}function Circle.area(radius) return math.pi * radius * radiusendfunction Circle.circumference(radius) return 2 * math.pi * radiusendreturn Circle
Organize your code into modules instead of using global functions and variables. This improves maintainability and reduces the risk of name collisions.
Using Tables Inefficiently
-- Anti-pattern: Inefficient table usagelocal list = {}for i = 1, 1000 do table.insert(list, i) -- Inefficient for large listsend-- Better approach: Pre-allocate indiceslocal list = {}for i = 1, 1000 do list[i] = i -- More efficient direct assignmentend
Use direct index assignment instead of table.insert() when you know the index in advance. This is more efficient, especially for large tables.
String Concatenation in Loops
-- Anti-pattern: String concatenation in loopslocal result = ""for i = 1, 1000 do result = result .. "item " .. i .. "\n" -- Creates many intermediate stringsend-- Better approach: Use table.concatlocal parts = {}for i = 1, 1000 do parts[i] = "item " .. iendlocal result = table.concat(parts, "\n")
Avoid concatenating strings in loops with the .. operator. Instead, collect strings in a table and use table.concat() at the end, which is much more efficient.
Not Using Proper Error Handling
-- Anti-pattern: No error handlingfunction read_file(filename) local file = io.open(filename, "r") -- Might fail local content = file:read("*all") file:close() return contentend-- Better approach: Proper error handlingfunction read_file(filename) local file, err = io.open(filename, "r") if not file then return nil, "Failed to open file: " .. (err or "unknown error") end local content, err = file:read("*all") file:close() if not content then return nil, "Failed to read file: " .. (err or "unknown error") end return contentend-- Usagelocal content, err = read_file("data.txt")if not content then print("Error: " .. err)else -- Process contentend
Use proper error handling by checking return values and returning error messages. Lua functions often return nil plus an error message on failure.
Using Numeric For-Loops Incorrectly
-- Anti-pattern: Incorrect numeric for-looplocal t = {10, 20, 30, 40, 50}for i = 0, #t do -- Starts at 0, but Lua tables are 1-indexed print(t[i]) -- t[0] is nilend-- Better approach: Correct indexinglocal t = {10, 20, 30, 40, 50}for i = 1, #t do -- Lua tables are 1-indexed print(t[i])end
Remember that Lua tables are 1-indexed by default. Start your numeric for-loops at 1 when iterating over sequential tables.
Not Using Proper Scope
-- Anti-pattern: Variables with too broad scopefunction process_data(items) count = 0 -- Global variable for i = 1, #items do if is_valid(items[i]) then count = count + 1 end end return countend-- Better approach: Proper variable scopingfunction process_data(items) local count = 0 -- Local variable for i = 1, #items do if is_valid(items[i]) then count = count + 1 end end return countend
Keep variables in the smallest possible scope. Use local variables instead of global ones whenever possible.
Inefficient Table Clearing
-- Anti-pattern: Inefficient table clearingfunction clear_table(t) t = {} -- This creates a new table, doesn't clear the originalend-- Better approach: Actually clear the tablefunction clear_table(t) for k in pairs(t) do t[k] = nil endend
Assigning a new empty table to a variable doesn’t clear the original table. Instead, set each key to nil to actually clear a table.
Not Using Metatables Appropriately
-- Anti-pattern: Reinventing common patternslocal Point = {}function Point.new(x, y) local self = {} self.x = x or 0 self.y = y or 0 function self.add(other) return Point.new(self.x + other.x, self.y + other.y) end return selfend-- Better approach: Use metatableslocal Point = {}Point.__index = Pointfunction Point.new(x, y) local self = setmetatable({}, Point) self.x = x or 0 self.y = y or 0 return selfendfunction Point:add(other) return Point.new(self.x + other.x, self.y + other.y)end-- Even better: Operator overloading with metatablesfunction Point.__add(a, b) return Point.new(a.x + b.x, a.y + b.y)end-- Usagelocal p1 = Point.new(1, 2)local p2 = Point.new(3, 4)local p3 = p1 + p2 -- Uses __add metamethod
Use metatables to implement object-oriented patterns, operator overloading, and other advanced features in a clean and efficient way.
Using Inefficient Patterns for OOP
-- Anti-pattern: Inefficient OOP patternfunction create_person(name, age) local person = {} person.name = name person.age = age person.greet = function(self) return "Hello, I'm " .. self.name end person.have_birthday = function(self) self.age = self.age + 1 end return personend-- Better approach: Prototype-based OOPlocal Person = {}Person.__index = Personfunction Person.new(name, age) local self = setmetatable({}, Person) self.name = name self.age = age return selfendfunction Person:greet() return "Hello, I'm " .. self.nameendfunction Person:have_birthday() self.age = self.age + 1end
Use prototype-based OOP with metatables instead of creating new function objects for each instance. This is more memory-efficient and performs better.
Not Using Proper Closures
-- Anti-pattern: Not using closures effectivelyfunction create_counter() local counter = {count = 0} function counter.increment() counter.count = counter.count + 1 return counter.count end function counter.get() return counter.count end return counterend-- Better approach: Use closuresfunction create_counter() local count = 0 return { increment = function() count = count + 1 return count end, get = function() return count end }end
Use closures to encapsulate state without exposing it directly. This provides better encapsulation and can be more memory-efficient.
Not Using Proper Module Structure
-- Anti-pattern: Poor module structure-- mymodule.luafunction hello() print("Hello, World!")endfunction goodbye() print("Goodbye, World!")end-- These are now global functions-- Better approach: Proper module structure-- mymodule.lualocal M = {}function M.hello() print("Hello, World!")endfunction M.goodbye() print("Goodbye, World!")endreturn M-- Usage-- local mymodule = require("mymodule")-- mymodule.hello()
Structure your modules properly by returning a table of functions and values. This prevents polluting the global namespace.
Using Unnecessary Upvalues
-- Anti-pattern: Unnecessary upvaluesfunction create_functions() local functions = {} for i = 1, 10 do functions[i] = function() return i end -- i is an upvalue end return functionsend-- Better approach: Avoid unnecessary upvaluesfunction create_functions() local functions = {} for i = 1, 10 do local value = i -- Create a new local variable functions[i] = function() return value end end return functionsend
Be careful with upvalues in loops. Create a new local variable inside the loop to capture the current value, not the final value of the loop variable.
Not Using Proper Error Propagation
-- Anti-pattern: Swallowing errorsfunction process_file(filename) local file = io.open(filename, "r") if not file then print("Error opening file") return end -- Process file... file:close()end-- Better approach: Propagate errorsfunction process_file(filename) local file, err = io.open(filename, "r") if not file then return nil, "Error opening file: " .. err end -- Process file... local result = file:read("*all") file:close() return resultend-- Usagelocal result, err = process_file("data.txt")if not result then -- Handle error print(err)else -- Use resultend
Propagate errors up the call stack instead of handling them locally or swallowing them. This allows the caller to decide how to handle errors.
Using Inefficient Table Traversal
-- Anti-pattern: Inefficient table traversallocal t = {a = 1, b = 2, c = 3}for i = 1, #t do -- This only works for array-like tables print(t[i])end-- Better approach: Use pairs for hash tableslocal t = {a = 1, b = 2, c = 3}for k, v in pairs(t) do print(k, v)end-- Use ipairs for array-like tableslocal arr = {10, 20, 30}for i, v in ipairs(arr) do print(i, v)end
Use pairs() for traversing hash tables and ipairs() for traversing array-like tables. Don’t use the length operator (#) for non-sequential tables.
Not Using Proper Documentation
-- Anti-pattern: Poor or no documentationfunction process_data(data, options) -- Implementation...end-- Better approach: Proper documentation---Process data according to specified options---@param data table The data to process---@param options table|nil Optional settings with the following fields:--- - verbose (boolean): Whether to output verbose logs--- - max_items (number): Maximum number of items to process---@return table The processed data---@return string|nil Error message if an error occurredfunction process_data(data, options) -- Implementation...end
Document your functions and modules properly. Include parameter descriptions, return values, and usage examples. Consider using a documentation format like LuaDoc or EmmyLua annotations.
Not Using Proper Testing
-- Anti-pattern: Manual testing or no testingfunction add(a, b) return a + bend-- Manual testprint(add(2, 3) == 5 and "OK" or "FAIL")-- Better approach: Use a testing frameworklocal luaunit = require("luaunit")function add(a, b) return a + bendfunction test_add() luaunit.assertEquals(add(2, 3), 5) luaunit.assertEquals(add(-1, 1), 0) luaunit.assertEquals(add(0, 0), 0)endluaunit.run("test_add")
Write proper tests for your code using a testing framework like LuaUnit, Busted, or luatest. This makes it easier to verify that your code works as expected and to catch regressions.
Using Inefficient Algorithms
-- Anti-pattern: Inefficient algorithmsfunction find_element(t, value) for i = 1, #t do if t[i] == value then return i end end return nilend-- Better approach: Use more efficient data structuresfunction create_lookup_table(t) local lookup = {} for i, v in ipairs(t) do lookup[v] = i end return lookupend-- Usagelocal data = {10, 20, 30, 40, 50}local lookup = create_lookup_table(data)-- O(1) lookup instead of O(n)local index = lookup[30] -- Returns 3
Choose appropriate algorithms and data structures for your task. Consider time and space complexity, especially for operations that are performed frequently or with large data sets.
Not Using Proper Resource Management
-- Anti-pattern: Poor resource managementfunction process_file(filename) local file = io.open(filename, "r") if not file then return nil end if some_condition() then return early_result -- File is never closed! end -- Process file... file:close() return resultend-- Better approach: Ensure resources are releasedfunction process_file(filename) local file, err = io.open(filename, "r") if not file then return nil, err end -- Use pcall to ensure file is closed even if an error occurs local success, result = pcall(function() if some_condition() then return early_result end -- Process file... return final_result end) file:close() -- Always close the file if not success then return nil, result -- result contains the error message end return resultend
Ensure that resources like files, network connections, and database connections are properly closed, even in error cases or early returns.
Not Using Proper Configuration Management
-- Anti-pattern: Hardcoded configurationfunction connect_to_database() return db.connect("localhost", 5432, "mydb", "user", "password")end-- Better approach: Externalize configurationlocal config = require("config")function connect_to_database() return db.connect( config.db.host, config.db.port, config.db.name, config.db.user, config.db.password )end-- config.luareturn { db = { host = os.getenv("DB_HOST") or "localhost", port = tonumber(os.getenv("DB_PORT")) or 5432, name = os.getenv("DB_NAME") or "mydb", user = os.getenv("DB_USER") or "user", password = os.getenv("DB_PASSWORD") or "password" }}
Externalize configuration instead of hardcoding values. This makes your code more flexible and easier to deploy in different environments.