Lua - Error Handling
Error Handling
Learn comprehensive error handling in Lua using pcall, xpcall, and custom error handling patterns for building robust, fault-tolerant applications.
Estimated time: 25-30 minutes
Learning Objectives
- Understand Lua's error handling mechanisms
- Use pcall and xpcall for protected function calls
- Create custom error types and handlers
- Implement error recovery strategies
- Build resilient applications with proper error management
Basic Error Concepts
Lua uses exceptions for error handling. When an error occurs, execution stops and an error message is generated.
-- Basic error demonstration
local function demonstrate_errors()
print("1. Normal execution")
-- This will cause an error
-- error("Something went wrong!") -- Uncomment to see error
print("2. This line executes if no error above")
-- Trying to call nil as function
local func = nil
-- func() -- Uncomment to see "attempt to call a nil value" error
-- Division by zero (actually returns inf in Lua)
print("3. Division by zero:", 10/0)
-- Accessing non-existent table field (returns nil, no error)
local t = {}
print("4. Non-existent field:", t.missing)
-- This WILL cause an error - indexing nil
-- print("5. Indexing nil:", t.missing.field) -- Uncomment to see error
end
demonstrate_errors()
Expected Output:
1. Normal execution
2. This line executes if no error above
3. Division by zero: inf
4. Non-existent field: nil
Protected Calls with pcall
pcall
(protected call) allows you to catch errors without stopping program execution.
-- Using pcall for error handling
local function pcall_examples()
-- Function that might fail
local function risky_division(a, b)
if b == 0 then
error("Cannot divide by zero")
end
return a / b
end
-- Safe call using pcall
local function safe_divide(a, b)
local success, result = pcall(risky_division, a, b)
if success then
print("Division successful:", result)
return result
else
print("Division failed:", result) -- result contains error message
return nil
end
end
print("=== pcall Examples ===")
-- Successful call
safe_divide(10, 2)
-- Failed call
safe_divide(10, 0)
-- Multiple operations with error handling
local function process_data(data)
-- Validate input
if type(data) ~= "table" then
error("Expected table, got " .. type(data))
end
-- Process each item
local results = {}
for i, value in ipairs(data) do
if type(value) ~= "number" then
error("Item " .. i .. " is not a number: " .. tostring(value))
end
table.insert(results, value * 2)
end
return results
end
-- Test with valid data
local success, result = pcall(process_data, {1, 2, 3, 4})
if success then
print("Processing successful:", table.concat(result, ", "))
else
print("Processing failed:", result)
end
-- Test with invalid data
success, result = pcall(process_data, {1, "invalid", 3})
if success then
print("Processing successful:", table.concat(result, ", "))
else
print("Processing failed:", result)
end
-- Test with wrong type
success, result = pcall(process_data, "not a table")
if success then
print("Processing successful:", table.concat(result, ", "))
else
print("Processing failed:", result)
end
end
pcall_examples()
Expected Output:
=== pcall Examples ===
Division successful: 5.0
Division failed: Cannot divide by zero
Processing successful: 2, 4, 6, 8
Processing failed: Item 2 is not a number: invalid
Processing failed: Expected table, got string
Extended Error Handling with xpcall
xpcall
allows you to specify a custom error handler for more detailed error information.
-- Using xpcall with custom error handlers
local function xpcall_examples()
-- Error handler that provides stack trace
local function error_handler(err)
local trace = debug.traceback()
return {
message = err,
trace = trace,
timestamp = os.date("%Y-%m-%d %H:%M:%S")
}
end
-- Function that might fail deeply nested
local function level3()
error("Deep error occurred")
end
local function level2()
level3()
end
local function level1()
level2()
end
print("=== xpcall with Custom Error Handler ===")
-- Using xpcall with error handler
local success, result = xpcall(level1, error_handler)
if success then
print("Function executed successfully")
else
print("Error occurred:")
print("Message:", result.message)
print("Timestamp:", result.timestamp)
print("Stack trace:")
print(result.trace)
end
-- Simplified error handler for logging
local function simple_error_handler(err)
local info = debug.getinfo(2, "Sl")
return string.format("[%s:%d] %s",
info.short_src or "unknown",
info.currentline or 0,
err)
end
-- File operation with error handling
local function read_config_file(filename)
local file = io.open(filename, "r")
if not file then
error("Could not open config file: " .. filename)
end
local content = file:read("*a")
file:close()
-- Simulate parsing error
if content:find("invalid") then
error("Invalid configuration detected")
end
return content
end
print("\n=== File Operation Error Handling ===")
-- Try to read non-existent file
success, result = xpcall(function()
return read_config_file("nonexistent.txt")
end, simple_error_handler)
if success then
print("Config loaded successfully")
else
print("Failed to load config:", result)
end
end
xpcall_examples()
Expected Output:
=== xpcall with Custom Error Handler ===
Error occurred:
Message: Deep error occurred
Timestamp: 2024-01-15 14:30:45
Stack trace:
stack traceback:
[string "..."]:6: in function 'level3'
[string "..."]:10: in function 'level2'
[string "..."]:14: in function 'level1'
...
=== File Operation Error Handling ===
Failed to load config: [string "..."]:45] Could not open config file: nonexistent.txt
Custom Error Types and Classes
Create structured error handling with custom error types for different failure scenarios.
-- Custom error type system
local ErrorType = {
VALIDATION = "ValidationError",
FILE_IO = "FileIOError",
NETWORK = "NetworkError",
DATABASE = "DatabaseError"
}
-- Error factory
local function create_error(error_type, message, context)
return {
type = error_type,
message = message,
context = context or {},
timestamp = os.time(),
stack_trace = debug.traceback()
}
end
-- Enhanced error handling class
local ErrorHandler = {}
ErrorHandler.__index = ErrorHandler
function ErrorHandler.new()
local self = setmetatable({}, ErrorHandler)
self.error_log = {}
self.handlers = {}
return self
end
function ErrorHandler:register_handler(error_type, handler_func)
self.handlers[error_type] = handler_func
end
function ErrorHandler:handle_error(err)
-- Log the error
table.insert(self.error_log, err)
-- Call specific handler if available
local handler = self.handlers[err.type]
if handler then
return handler(err)
end
-- Default handler
print(string.format("[%s] %s: %s",
os.date("%H:%M:%S", err.timestamp),
err.type,
err.message))
if next(err.context) then
print("Context:", table.concat(err.context, ", "))
end
return false -- indicate error was not recovered
end
function ErrorHandler:get_error_count(error_type)
local count = 0
for _, err in ipairs(self.error_log) do
if not error_type or err.type == error_type then
count = count + 1
end
end
return count
end
-- Example application with structured error handling
local function demo_custom_error_handling()
local error_handler = ErrorHandler.new()
-- Register specific error handlers
error_handler:register_handler(ErrorType.VALIDATION, function(err)
print("VALIDATION ERROR: " .. err.message)
if err.context.field then
print("Field: " .. err.context.field)
end
return true -- error handled
end)
error_handler:register_handler(ErrorType.FILE_IO, function(err)
print("FILE I/O ERROR: " .. err.message)
if err.context.filename then
print("Attempting fallback for file: " .. err.context.filename)
-- Could implement fallback logic here
end
return false -- error not fully recovered
end)
-- Simulated application functions
local function validate_user_input(user_data)
if not user_data.name or user_data.name == "" then
local err = create_error(ErrorType.VALIDATION,
"Name is required",
{field = "name", value = user_data.name})
error(err)
end
if not user_data.age or user_data.age < 0 or user_data.age > 150 then
local err = create_error(ErrorType.VALIDATION,
"Age must be between 0 and 150",
{field = "age", value = user_data.age})
error(err)
end
return true
end
local function save_user_data(user_data, filename)
local file = io.open(filename, "w")
if not file then
local err = create_error(ErrorType.FILE_IO,
"Cannot open file for writing",
{filename = filename})
error(err)
end
file:write(string.format("Name: %s\nAge: %d\n",
user_data.name, user_data.age))
file:close()
return true
end
-- Test the error handling system
local test_cases = {
{name = "John Doe", age = 25}, -- Valid
{name = "", age = 30}, -- Invalid name
{name = "Jane Smith", age = -5}, -- Invalid age
{name = "Bob Wilson", age = 35} -- Valid but will fail on file I/O
}
print("=== Custom Error Handling Demo ===")
for i, user_data in ipairs(test_cases) do
print(string.format("\nProcessing user %d: %s", i, user_data.name or "(empty)"))
-- Validation step
local success, result = xpcall(function()
return validate_user_input(user_data)
end, function(err)
return err
end)
if success then
print("Validation passed")
-- File I/O step (will fail for demonstration)
local filename = i == 4 and "/invalid/path/user.txt" or "user_" .. i .. ".txt"
success, result = xpcall(function()
return save_user_data(user_data, filename)
end, function(err)
return err
end)
if success then
print("User data saved successfully")
else
error_handler:handle_error(result)
end
else
error_handler:handle_error(result)
end
end
-- Display error statistics
print(string.format("\n=== Error Statistics ==="))
print("Total errors:", error_handler:get_error_count())
print("Validation errors:", error_handler:get_error_count(ErrorType.VALIDATION))
print("File I/O errors:", error_handler:get_error_count(ErrorType.FILE_IO))
end
demo_custom_error_handling()
Expected Output:
=== Custom Error Handling Demo ===
Processing user 1: John Doe
Validation passed
User data saved successfully
Processing user 2: (empty)
VALIDATION ERROR: Name is required
Field: name
Processing user 3: Jane Smith
VALIDATION ERROR: Age must be between 0 and 150
Field: age
Processing user 4: Bob Wilson
Validation passed
FILE I/O ERROR: Cannot open file for writing
Attempting fallback for file: /invalid/path/user.txt
=== Error Statistics ===
Total errors: 3
Validation errors: 2
File I/O errors: 1
Error Recovery Patterns
Implement common error recovery strategies for resilient applications.
-- Retry mechanism with exponential backoff
local function create_retry_wrapper(max_attempts, base_delay)
max_attempts = max_attempts or 3
base_delay = base_delay or 1
return function(func, ...)
local args = {...}
for attempt = 1, max_attempts do
local success, result = pcall(func, table.unpack(args))
if success then
if attempt > 1 then
print(string.format("Operation succeeded on attempt %d", attempt))
end
return result
end
print(string.format("Attempt %d failed: %s", attempt, result))
if attempt < max_attempts then
local delay = base_delay * (2 ^ (attempt - 1)) -- exponential backoff
print(string.format("Retrying in %d seconds...", delay))
-- In real application, you'd use actual sleep function
-- os.execute("sleep " .. delay) -- Unix/Linux
end
end
error("All retry attempts failed")
end
end
-- Circuit breaker pattern
local CircuitBreaker = {}
CircuitBreaker.__index = CircuitBreaker
function CircuitBreaker.new(failure_threshold, recovery_timeout)
local self = setmetatable({}, CircuitBreaker)
self.failure_threshold = failure_threshold or 5
self.recovery_timeout = recovery_timeout or 30
self.failure_count = 0
self.last_failure_time = 0
self.state = "closed" -- closed, open, half-open
return self
end
function CircuitBreaker:call(func, ...)
if self.state == "open" then
if os.time() - self.last_failure_time > self.recovery_timeout then
self.state = "half-open"
print("Circuit breaker transitioning to half-open")
else
error("Circuit breaker is OPEN - fast failing")
end
end
local success, result = pcall(func, ...)
if success then
if self.state == "half-open" then
self.state = "closed"
self.failure_count = 0
print("Circuit breaker CLOSED - service recovered")
end
return result
else
self.failure_count = self.failure_count + 1
self.last_failure_time = os.time()
if self.failure_count >= self.failure_threshold then
self.state = "open"
print(string.format("Circuit breaker OPENED after %d failures",
self.failure_count))
end
error(result)
end
end
-- Demo error recovery patterns
local function demo_error_recovery()
print("=== Error Recovery Patterns Demo ===")
-- Unreliable service simulation
local service_call_count = 0
local function unreliable_service()
service_call_count = service_call_count + 1
-- Fail first 2 times, succeed on 3rd
if service_call_count <= 2 then
error("Service temporarily unavailable")
end
return "Service response: Success!"
end
-- Test retry mechanism
print("\n--- Retry Mechanism Test ---")
local retry_func = create_retry_wrapper(4, 1)
local success, result = pcall(retry_func, unreliable_service)
if success then
print("Final result:", result)
else
print("All retries failed:", result)
end
-- Test circuit breaker
print("\n--- Circuit Breaker Test ---")
local circuit_breaker = CircuitBreaker.new(3, 5)
local function failing_service()
error("Service is down")
end
-- Simulate multiple calls to failing service
for i = 1, 7 do
local success, result = pcall(function()
return circuit_breaker:call(failing_service)
end)
if success then
print(string.format("Call %d: Success - %s", i, result))
else
print(string.format("Call %d: Failed - %s", i, result))
end
-- Brief pause simulation
if i == 5 then
print("--- Waiting for recovery timeout ---")
end
end
end
demo_error_recovery()
Expected Output:
=== Error Recovery Patterns Demo ===
--- Retry Mechanism Test ---
Attempt 1 failed: Service temporarily unavailable
Retrying in 1 seconds...
Attempt 2 failed: Service temporarily unavailable
Retrying in 2 seconds...
Operation succeeded on attempt 3
Final result: Service response: Success!
--- Circuit Breaker Test ---
Call 1: Failed - Service is down
Call 2: Failed - Service is down
Call 3: Failed - Service is down
Circuit breaker OPENED after 3 failures
Call 4: Failed - Circuit breaker is OPEN - fast failing
Call 5: Failed - Circuit breaker is OPEN - fast failing
--- Waiting for recovery timeout ---
Call 6: Failed - Circuit breaker is OPEN - fast failing
Call 7: Failed - Circuit breaker is OPEN - fast failing
Common Pitfalls
Error Handling Best Practices
- Don't ignore errors: Always handle or propagate errors appropriately
- Use specific error messages: Include context about what went wrong
- Log errors: Maintain error logs for debugging and monitoring
- Clean up resources: Ensure files, connections are closed even on errors
- Fail fast: Don't continue with invalid data
Checks for Understanding
- What's the difference between pcall and xpcall?
- When should you use error recovery vs. failing fast?
- What information should a good error message contain?
- How can you prevent resource leaks during error conditions?
Show answers
- xpcall allows custom error handler function for enhanced error info
- Recovery for transient issues, fail fast for logic errors or invalid data
- What happened, where, when, and possibly how to fix it
- Use finally-like patterns or ensure cleanup in error handlers
Exercises
- Create a robust file downloader with retry logic
- Implement a validation framework with custom error types
- Build a circuit breaker for database operations