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

  1. What is the simplest form of debugging in Lua?
  2. How do you get information about the current function using the debug library?
  3. What's the difference between debug.getinfo() levels 1 and 2?
  4. How can you create a custom error handler that shows a stack trace?
  5. What are debug hooks and when would you use them?
Show answers
  1. Print debugging - using strategic print() statements to output variable values and execution flow.
  2. Use debug.getinfo(1, "nSl") to get name, source, and line information about the current function.
  3. Level 1 is the current function, level 2 is the calling function (one level up the stack).
  4. Create a function that takes an error message and returns debug.traceback(), then use it with xpcall().
  5. 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.