Skip to main content
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.
-- Anti-pattern: Implicit global variables
function process_data()
  result = 42  -- Implicitly global!
  return result
end

-- Better approach: Use local variables
function process_data()
  local result = 42  -- Explicitly local
  return result
end
Always use the local keyword when declaring variables. In Lua, variables are global by default, which can lead to unexpected behavior and name collisions.
-- Anti-pattern: Global functions and variables
function calculate_area(radius)
  return math.pi * radius * radius
end

function calculate_circumference(radius)
  return 2 * math.pi * radius
end

-- Better approach: Use modules
local Circle = {}

function Circle.area(radius)
  return math.pi * radius * radius
end

function Circle.circumference(radius)
  return 2 * math.pi * radius
end

return Circle
Organize your code into modules instead of using global functions and variables. This improves maintainability and reduces the risk of name collisions.
-- Anti-pattern: Inefficient table usage
local list = {}
for i = 1, 1000 do
  table.insert(list, i)  -- Inefficient for large lists
end

-- Better approach: Pre-allocate indices
local list = {}
for i = 1, 1000 do
  list[i] = i  -- More efficient direct assignment
end
Use direct index assignment instead of table.insert() when you know the index in advance. This is more efficient, especially for large tables.
-- Anti-pattern: String concatenation in loops
local result = ""
for i = 1, 1000 do
  result = result .. "item " .. i .. "\n"  -- Creates many intermediate strings
end

-- Better approach: Use table.concat
local parts = {}
for i = 1, 1000 do
  parts[i] = "item " .. i
end
local 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.
-- Anti-pattern: No error handling
function read_file(filename)
  local file = io.open(filename, "r")  -- Might fail
  local content = file:read("*all")
  file:close()
  return content
end

-- Better approach: Proper error handling
function 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 content
end

-- Usage
local content, err = read_file("data.txt")
if not content then
  print("Error: " .. err)
else
  -- Process content
end
Use proper error handling by checking return values and returning error messages. Lua functions often return nil plus an error message on failure.
-- Anti-pattern: Incorrect numeric for-loop
local 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 nil
end

-- Better approach: Correct indexing
local 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.
-- Anti-pattern: Variables with too broad scope
function process_data(items)
  count = 0  -- Global variable
  for i = 1, #items do
    if is_valid(items[i]) then
      count = count + 1
    end
  end
  return count
end

-- Better approach: Proper variable scoping
function 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 count
end
Keep variables in the smallest possible scope. Use local variables instead of global ones whenever possible.
-- Anti-pattern: Inefficient table clearing
function clear_table(t)
  t = {}  -- This creates a new table, doesn't clear the original
end

-- Better approach: Actually clear the table
function clear_table(t)
  for k in pairs(t) do
    t[k] = nil
  end
end
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.
-- Anti-pattern: Reinventing common patterns
local 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 self
end

-- Better approach: Use metatables
local Point = {}
Point.__index = Point

function Point.new(x, y)
  local self = setmetatable({}, Point)
  self.x = x or 0
  self.y = y or 0
  return self
end

function Point:add(other)
  return Point.new(self.x + other.x, self.y + other.y)
end

-- Even better: Operator overloading with metatables
function Point.__add(a, b)
  return Point.new(a.x + b.x, a.y + b.y)
end

-- Usage
local 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.
-- Anti-pattern: Inefficient OOP pattern
function 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 person
end

-- Better approach: Prototype-based OOP
local Person = {}
Person.__index = Person

function Person.new(name, age)
  local self = setmetatable({}, Person)
  self.name = name
  self.age = age
  return self
end

function Person:greet()
  return "Hello, I'm " .. self.name
end

function Person:have_birthday()
  self.age = self.age + 1
end
Use prototype-based OOP with metatables instead of creating new function objects for each instance. This is more memory-efficient and performs better.
-- Anti-pattern: Not using closures effectively
function 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 counter
end

-- Better approach: Use closures
function 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.
-- Anti-pattern: Poor module structure
-- mymodule.lua
function hello()
  print("Hello, World!")
end

function goodbye()
  print("Goodbye, World!")
end

-- These are now global functions

-- Better approach: Proper module structure
-- mymodule.lua
local M = {}

function M.hello()
  print("Hello, World!")
end

function M.goodbye()
  print("Goodbye, World!")
end

return 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.
-- Anti-pattern: Unnecessary upvalues
function create_functions()
  local functions = {}
  
  for i = 1, 10 do
    functions[i] = function() return i end  -- i is an upvalue
  end
  
  return functions
end

-- Better approach: Avoid unnecessary upvalues
function 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 functions
end
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.
-- Anti-pattern: Swallowing errors
function 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 errors
function 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 result
end

-- Usage
local result, err = process_file("data.txt")
if not result then
  -- Handle error
  print(err)
else
  -- Use result
end
Propagate errors up the call stack instead of handling them locally or swallowing them. This allows the caller to decide how to handle errors.
-- Anti-pattern: Inefficient table traversal
local 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 tables
local t = {a = 1, b = 2, c = 3}

for k, v in pairs(t) do
  print(k, v)
end

-- Use ipairs for array-like tables
local 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.
-- Anti-pattern: Poor or no documentation
function 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 occurred
function 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.
-- Anti-pattern: Manual testing or no testing
function add(a, b)
  return a + b
end

-- Manual test
print(add(2, 3) == 5 and "OK" or "FAIL")

-- Better approach: Use a testing framework
local luaunit = require("luaunit")

function add(a, b)
  return a + b
end

function test_add()
  luaunit.assertEquals(add(2, 3), 5)
  luaunit.assertEquals(add(-1, 1), 0)
  luaunit.assertEquals(add(0, 0), 0)
end

luaunit.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.
-- Anti-pattern: Inefficient algorithms
function find_element(t, value)
  for i = 1, #t do
    if t[i] == value then
      return i
    end
  end
  return nil
end

-- Better approach: Use more efficient data structures
function create_lookup_table(t)
  local lookup = {}
  for i, v in ipairs(t) do
    lookup[v] = i
  end
  return lookup
end

-- Usage
local 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.
-- Anti-pattern: Poor resource management
function 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 result
end

-- Better approach: Ensure resources are released
function 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 result
end
Ensure that resources like files, network connections, and database connections are properly closed, even in error cases or early returns.
-- Anti-pattern: Hardcoded configuration
function connect_to_database()
  return db.connect("localhost", 5432, "mydb", "user", "password")
end

-- Better approach: Externalize configuration
local 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.lua
return {
  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.
I