Lua - Debugging
Overview
Estimated time: 25–30 minutes
Debugging is essential for finding and fixing errors in your Lua programs. This tutorial covers debugging techniques, the debug library, error handling strategies, and tools to help you write robust Lua code.
Learning Objectives
- Use print debugging and strategic logging techniques
- Master the Lua debug library functions
- Understand stack traces and error information
- Apply advanced debugging patterns and tools
- Implement effective error handling strategies
Prerequisites
- Understanding of Lua functions and scope
- Knowledge of error handling with pcall/xpcall
- Basic understanding of stack concepts
Print Debugging
The simplest debugging technique is strategic print statements:
-- Basic print debugging
local function calculate_average(numbers)
print("DEBUG: calculate_average called with", #numbers, "numbers")
if #numbers == 0 then
print("DEBUG: Empty array detected")
return 0
end
local sum = 0
for i, num in ipairs(numbers) do
print("DEBUG: Processing number", i, ":", num)
sum = sum + num
end
local average = sum / #numbers
print("DEBUG: Calculated average:", average)
return average
end
local data = {10, 20, 30, 40}
local result = calculate_average(data)
print("Result:", result)
Expected Output:
DEBUG: calculate_average called with 4 numbers
DEBUG: Processing number 1 : 10
DEBUG: Processing number 2 : 20
DEBUG: Processing number 3 : 30
DEBUG: Processing number 4 : 40
DEBUG: Calculated average: 25.0
Result: 25.0
The Debug Library
Lua's debug library provides powerful introspection capabilities:
Getting Stack Information
-- debug.getinfo() - get information about a function
local function example_function(param)
local info = debug.getinfo(1, "nSl")
print("Function name:", info.name or "anonymous")
print("Source file:", info.source)
print("Current line:", info.currentline)
print("Function defined at line:", info.linedefined)
end
example_function("test")
-- Stack trace function
local function print_stack_trace()
print("\n=== Stack Trace ===")
local level = 1
while true do
local info = debug.getinfo(level, "nSl")
if not info then break end
local name = info.name or "anonymous"
local source = info.source:match("@(.+)") or info.source
print(string.format(" [%d] %s at %s:%d",
level, name, source, info.currentline))
level = level + 1
end
print("===================\n")
end
local function func_a()
func_b()
end
local function func_b()
func_c()
end
local function func_c()
print_stack_trace()
end
func_a()
Accessing Local Variables
-- debug.getlocal() - access local variables
local function debug_locals(level)
print("Local variables at level", level, ":")
local i = 1
while true do
local name, value = debug.getlocal(level, i)
if not name then break end
if name:sub(1, 1) ~= "(" then -- Skip internal variables
print(" " .. name .. " =", value)
end
i = i + 1
end
end
local function example_with_locals()
local x = 42
local y = "hello"
local z = {1, 2, 3}
debug_locals(1) -- Current function level
end
example_with_locals()
Debug Hooks
Debug hooks allow you to execute code at specific events:
-- Line hook - executes on every line
local line_count = 0
local function line_hook()
line_count = line_count + 1
local info = debug.getinfo(2, "nSl")
if info and info.source ~= "=[C]" then
print(string.format("Line %d: %s:%d",
line_count, info.source, info.currentline))
end
end
-- Set the hook for line events
debug.sethook(line_hook, "l")
-- Test code
local a = 10
local b = 20
local c = a + b
print("Sum:", c)
-- Remove the hook
debug.sethook()
-- Function call hook
local call_depth = 0
local function call_hook(event)
local info = debug.getinfo(2, "n")
local name = info.name or "anonymous"
if event == "call" then
call_depth = call_depth + 1
print(string.rep(" ", call_depth - 1) .. "-> " .. name)
elseif event == "return" then
print(string.rep(" ", call_depth - 1) .. "<- " .. name)
call_depth = call_depth - 1
end
end
-- Set hook for function calls and returns
debug.sethook(call_hook, "cr")
local function factorial(n)
if n <= 1 then
return 1
else
return n * factorial(n - 1)
end
end
factorial(4)
-- Remove the hook
debug.sethook()
Error Information and Tracebacks
Getting detailed error information:
-- Custom error handler with traceback
local function error_handler(err)
print("ERROR:", err)
print("Stack traceback:")
print(debug.traceback())
return err
end
-- Using xpcall with custom error handler
local function risky_function()
local function level3()
error("Something went wrong!")
end
local function level2()
level3()
end
local function level1()
level2()
end
level1()
end
local success, result = xpcall(risky_function, error_handler)
print("Success:", success, "Result:", result)
-- Simple traceback function
local function simple_traceback()
return debug.traceback("Custom traceback:", 2)
end
local function test_traceback()
print(simple_traceback())
end
test_traceback()
Debugging Best Practices
Conditional Debug Output
-- Debug flag for controlling output
local DEBUG = true
local function debug_print(...)
if DEBUG then
print("DEBUG:", ...)
end
end
-- Usage
local function process_data(data)
debug_print("Processing data with", #data, "items")
for i, item in ipairs(data) do
debug_print("Item", i, ":", item)
-- Process item...
end
debug_print("Processing complete")
end
process_data({1, 2, 3})
Debug Information Table
-- Comprehensive debug information function
local function get_debug_info(level)
level = level or 1
local info = debug.getinfo(level + 1, "nSluf")
if not info then return nil end
local result = {
name = info.name or "anonymous",
source = info.source,
currentline = info.currentline,
linedefined = info.linedefined,
what = info.what,
nups = info.nups,
nparams = info.nparams,
isvararg = info.isvararg,
locals = {}
}
-- Get local variables
local i = 1
while true do
local name, value = debug.getlocal(level + 1, i)
if not name then break end
if name:sub(1, 1) ~= "(" then
result.locals[name] = value
end
i = i + 1
end
return result
end
local function example_debug_info(x, y)
local z = x + y
local info = get_debug_info()
print("Function:", info.name)
print("Parameters:", info.nparams)
print("Local variables:")
for name, value in pairs(info.locals) do
print(" " .. name .. " =", value)
end
end
example_debug_info(10, 20)
Debugging Tools and Techniques
Interactive Debugging
-- Simple interactive debugger
local function debug_prompt()
print("\nDebugger activated. Type 'c' to continue, 'q' to quit:")
while true do
io.write("debug> ")
local command = io.read()
if command == "c" then
break
elseif command == "q" then
os.exit()
elseif command == "bt" then
print(debug.traceback())
elseif command:match("^p ") then
local var_name = command:match("^p (.+)")
local level = 2
local found = false
-- Look for variable in stack
while true do
local i = 1
while true do
local name, value = debug.getlocal(level, i)
if not name then break end
if name == var_name then
print(var_name .. " =", value)
found = true
break
end
i = i + 1
end
if found then break end
local info = debug.getinfo(level)
if not info then break end
level = level + 1
end
if not found then
print("Variable '" .. var_name .. "' not found")
end
else
print("Commands: c (continue), q (quit), bt (backtrace), p (print variable)")
end
end
end
-- Insert debugging breakpoint
local function debug_break()
debug_prompt()
end
-- Example usage
local function test_interactive_debug()
local x = 42
local y = "test"
debug_break() -- Breakpoint here
print("After debug break")
end
-- Uncomment to test:
-- test_interactive_debug()
Performance Debugging
-- Simple profiler
local function profile_function(func, name)
return function(...)
local start_time = os.clock()
local results = {func(...)}
local end_time = os.clock()
print(string.format("Function '%s' took %.4f seconds",
name or "anonymous", end_time - start_time))
return table.unpack(results)
end
end
-- Example usage
local function slow_function()
local sum = 0
for i = 1, 1000000 do
sum = sum + i
end
return sum
end
local profiled_slow = profile_function(slow_function, "slow_function")
local result = profiled_slow()
print("Result:", result)
Common Debugging Scenarios
Nil Value Debugging
-- Safe table access with debugging
local function safe_get(table, key, context)
if type(table) ~= "table" then
print("DEBUG: Expected table but got", type(table), "in", context)
return nil
end
local value = table[key]
if value == nil then
print("DEBUG: Key '" .. tostring(key) .. "' not found in", context)
print("DEBUG: Available keys:", table.concat(vim.tbl_keys(table or {}), ", "))
end
return value
end
-- Example usage
local data = {name = "John", age = 30}
local name = safe_get(data, "name", "user data")
local height = safe_get(data, "height", "user data") -- Will show debug info
Loop Debugging
-- Debug loop iterations
local function debug_loop(items, max_debug_items)
max_debug_items = max_debug_items or 10
print("Processing", #items, "items")
for i, item in ipairs(items) do
if i <= max_debug_items then
print(string.format("Item %d/%d: %s", i, #items, tostring(item)))
elseif i == max_debug_items + 1 then
print("... (suppressing further debug output)")
end
-- Process item here
end
print("Loop completed")
end
local test_data = {}
for i = 1, 20 do
test_data[i] = "item_" .. i
end
debug_loop(test_data, 5)
Common Pitfalls
- Remember that debug functions add overhead - remove them in production
- Be careful with debug hooks as they can significantly slow down execution
- Debug library functions may not be available in some Lua environments
- Stack levels start at 1 for debug.getinfo() and debug.getlocal()
- Some debug information may not be available if debug symbols are stripped
Checks for Understanding
- What is the simplest form of debugging in Lua?
- How do you get information about the current function using the debug library?
- What's the difference between debug.getinfo() levels 1 and 2?
- How can you create a custom error handler that shows a stack trace?
- What are debug hooks and when would you use them?
Show answers
- Print debugging - using strategic print() statements to output variable values and execution flow.
- Use debug.getinfo(1, "nSl") to get name, source, and line information about the current function.
- Level 1 is the current function, level 2 is the calling function (one level up the stack).
- Create a function that takes an error message and returns debug.traceback(), then use it with xpcall().
- Debug hooks execute code at specific events (line, call, return). Use them for profiling, tracing, or step-by-step debugging.
Next Steps
Effective debugging is crucial for Lua development. With these techniques, you can quickly identify and fix issues in your code. Next, learn about style guides and best practices for writing maintainable Lua code.