Web Analytics

Error Handling in Lua

Intermediate ~25 min read

Robust error handling is crucial for building reliable applications. Lua provides several mechanisms for handling errors: error(), assert(), pcall(), and xpcall(). In this lesson, you'll learn how to handle errors gracefully and build resilient code. Let's explore error handling in Lua!

Raising Errors

Using error()

function divide(a, b)
    if b == 0 then
        error("Division by zero")
    end
    return a / b
end

-- This will raise an error
-- divide(10, 0)  -- Error: Division by zero

Error with Level

function validateAge(age)
    if type(age) ~= "number" then
        error("Age must be a number", 2)  -- Level 2: caller's location
    end
    if age < 0 or age > 150 then
        error("Age must be between 0 and 150", 2)
    end
    return true
end
Error Levels:
  • 1 (default): Error at current function
  • 2: Error at caller's location
  • 0: No location information

Using assert()

assert() raises an error if a condition is false:

function sqrt(x)
    assert(x >= 0, "Cannot take square root of negative number")
    return math.sqrt(x)
end

print(sqrt(16))  -- 4
-- print(sqrt(-4))  -- Error: Cannot take square root of negative number

Assert with Function Calls

-- Assert that a file opened successfully
local file = assert(io.open("data.txt", "r"), "Failed to open file")

-- Assert function return values
local function getConfig()
    return nil, "Configuration not found"
end

local config = assert(getConfig())  -- Error: Configuration not found
Output
Click Run to execute your code

Protected Calls with pcall()

pcall() (protected call) catches errors without stopping execution:

function riskyFunction()
    error("Something went wrong!")
end

local success, result = pcall(riskyFunction)

if success then
    print("Success:", result)
else
    print("Error:", result)  -- Error: Something went wrong!
end

pcall() with Arguments

function divide(a, b)
    if b == 0 then
        error("Division by zero")
    end
    return a / b
end

local success, result = pcall(divide, 10, 2)
print(success, result)  -- true  5

local success, error = pcall(divide, 10, 0)
print(success, error)  -- false  Division by zero

Practical pcall() Usage

local function safeRequire(module)
    local success, result = pcall(require, module)
    if success then
        return result
    else
        print("Warning: Failed to load module " .. module)
        return nil
    end
end

local json = safeRequire("json")
if json then
    -- Use json module
end
Output
Click Run to execute your code

xpcall() with Error Handler

xpcall() allows you to specify a custom error handler:

local function errorHandler(err)
    return "Error occurred: " .. tostring(err) .. "\n" .. debug.traceback()
end

local function riskyFunction()
    local x = nil
    return x.field  -- Error: attempt to index nil value
end

local success, result = xpcall(riskyFunction, errorHandler)

if not success then
    print(result)  -- Prints error with stack trace
end

Custom Error Handler

local function detailedErrorHandler(err)
    local info = {
        error = tostring(err),
        timestamp = os.date("%Y-%m-%d %H:%M:%S"),
        traceback = debug.traceback()
    }
    return info
end

local function buggyFunction()
    error("Oops!")
end

local success, errorInfo = xpcall(buggyFunction, detailedErrorHandler)

if not success then
    print("Error:", errorInfo.error)
    print("Time:", errorInfo.timestamp)
    print("Trace:", errorInfo.traceback)
end
Output
Click Run to execute your code

Error Handling Patterns

Return nil, error Pattern

local function readFile(filename)
    local file, err = io.open(filename, "r")
    if not file then
        return nil, "Failed to open file: " .. err
    end
    
    local content = file:read("*all")
    file:close()
    
    return content
end

local content, err = readFile("data.txt")
if not content then
    print("Error:", err)
else
    print("Content:", content)
end

Try-Catch Pattern

local function try(func, catch)
    local success, result = pcall(func)
    if not success then
        if catch then
            catch(result)
        end
        return nil
    end
    return result
end

-- Usage
try(function()
    error("Something went wrong")
end, function(err)
    print("Caught error:", err)
end)

Retry Pattern

local function retry(func, maxAttempts, delay)
    local attempts = 0
    
    while attempts < maxAttempts do
        attempts = attempts + 1
        local success, result = pcall(func)
        
        if success then
            return result
        end
        
        if attempts < maxAttempts then
            print("Attempt " .. attempts .. " failed, retrying...")
            -- In real code, you'd sleep here
        end
    end
    
    error("Failed after " .. maxAttempts .. " attempts")
end

-- Usage
local result = retry(function()
    -- Potentially failing operation
    if math.random() > 0.7 then
        return "Success!"
    else
        error("Random failure")
    end
end, 3)
Output
Click Run to execute your code

Validation and Error Messages

Input Validation

local function createUser(name, email, age)
    -- Validate name
    assert(type(name) == "string", "Name must be a string")
    assert(#name > 0, "Name cannot be empty")
    
    -- Validate email
    assert(type(email) == "string", "Email must be a string")
    assert(email:match("^[%w.]+@[%w.]+%.%w+$"), "Invalid email format")
    
    -- Validate age
    assert(type(age) == "number", "Age must be a number")
    assert(age >= 0 and age <= 150, "Age must be between 0 and 150")
    
    return {
        name = name,
        email = email,
        age = age
    }
end

local success, result = pcall(createUser, "Alice", "[email protected]", 25)
if success then
    print("User created:", result.name)
else
    print("Validation error:", result)
end

Custom Error Types

local ValidationError = {}
ValidationError.__index = ValidationError

function ValidationError:new(field, message)
    local self = setmetatable({}, ValidationError)
    self.type = "ValidationError"
    self.field = field
    self.message = message
    return self
end

function ValidationError:__tostring()
    return string.format("ValidationError [%s]: %s", self.field, self.message)
end

local function validateUser(user)
    if not user.name or #user.name == 0 then
        error(ValidationError:new("name", "Name is required"))
    end
    if not user.email or not user.email:match("@") then
        error(ValidationError:new("email", "Invalid email"))
    end
    return true
end

local success, err = pcall(validateUser, {name = "", email = "invalid"})
if not success then
    if type(err) == "table" and err.type == "ValidationError" then
        print("Validation failed:", err.field, err.message)
    else
        print("Unknown error:", err)
    end
end

Error Handling Best Practices

Best Practices:
  • Use pcall() for risky operations: File I/O, network calls, external modules
  • Provide meaningful error messages: Help users understand what went wrong
  • Validate early: Check inputs at function entry
  • Use assert() for preconditions: Document assumptions
  • Return nil, error for expected failures: Use error() for unexpected failures
  • Log errors appropriately: Include context and stack traces
  • Clean up resources: Use pcall() with cleanup in finally pattern

Practice Exercise

Try these error handling challenges:

Output
Click Run to execute your code

Summary

In this lesson, you learned:

  • Raising errors with error() and assert()
  • Protected calls with pcall()
  • Custom error handlers with xpcall()
  • Common error handling patterns (try-catch, retry, nil-error)
  • Input validation and custom error types
  • Error handling best practices

What's Next?

You've mastered error handling! Next, we'll explore debugging techniques in Lua. You'll learn how to use the debug library, print debugging, logging, and tools for finding and fixing bugs. Let's continue! 🚀