Skip to content

Lua Error Handling

Effective error handling is crucial in software development. Operations like file I/O, network requests, database interactions, or complex calculations can fail. Unhandled errors can lead to program crashes, data corruption, or poor user experience. Robust error handling makes applications more reliable and easier to debug.

In Lua, errors can generally be categorized into:

  • Syntax Errors: Mistakes in the structure or grammar of the Lua code. These are usually caught by the Lua compiler/parser before execution.
  • Runtime Errors: Errors that occur while the program is running, such as attempting an operation on an incorrect data type, division by zero, or trying to access a nil value as if it were a table.

Syntax errors are detected when Lua tries to compile the code. The interpreter will typically report the location and nature of the error.

-- Example of a syntax error: missing 'do' in a for loop
for i = 1, 10
print(i)
end

Running the above code might produce an error message like:

lua: SCRIPT_NAME.lua:2: 'do' expected near 'print'

Syntax errors are generally straightforward to fix as the error message often points directly to the problem.

Runtime errors occur during program execution. They can be caused by logical mistakes, invalid input, or unexpected conditions.

-- Example of a runtime error: attempting arithmetic on a nil value
local function add(a, b)
return a + b
end
add(10) -- 'b' will be nil inside the function

This code will compile, but running it will result in a runtime error:

lua: SCRIPT_NAME.lua:2: attempt to perform arithmetic on local 'b' (a nil value)
stack traceback:
SCRIPT_NAME.lua:2: in function 'add'
SCRIPT_NAME.lua:5: in main chunk
[C]: in ?

The error message indicates the type of error and provides a stack traceback to help locate where the error occurred.

assert(value, message): Checks if value is true (i.e., not false and not nil). If value is false or nil, assert raises an error. The optional message string is used as the error message. If value is true, assert returns all its arguments.

error(message [, level]): Raises an error with the given message. The optional level argument specifies how to get the error position in the call stack. Level 1 (default) points to where error was called. Level 2 points to where the function that called error was called, and so on. Level 0 avoids adding error position information.

Example using assert:

local function process_data(data)
assert(type(data) == "table", "Input must be a table")
assert(data.value, "Table must contain 'value' field")
assert(type(data.value) == "number", "'value' field must be a number")
-- Process data.value
print("Processing value:", data.value)
end
-- process_data({ value = 10 }) -- This would work
-- process_data({ name = "test" }) -- This will error on the second assert
process_data(nil) -- This will error on the first assert

If process_data(nil) is called, the output might be:

lua: SCRIPT_NAME.lua:2: Input must be a table
stack traceback:
[C]: in function 'assert'
SCRIPT_NAME.lua:2: in function 'process_data'
SCRIPT_NAME.lua:11: in main chunk
[C]: in ?

To handle errors gracefully without terminating the script, Lua provides pcall (protected call) and xpcall (extended protected call).

pcall(func, arg1, ...):

Calls the function func with the given arguments in protected mode. If no errors occur during func’s execution, pcall returns true followed by any results from func. If an error occurs, pcall catches it, returns false, and then the error message or error object.

local function might_fail(x, y)
if y == 0 then
error("division by zero")
end
return x / y
end
local status, result_or_error = pcall(might_fail, 10, 2)
if status then
print("Success:", result_or_error) -- Success: 5
else
print("Failure:", result_or_error) -- Error message
end
local status2, error_msg2 = pcall(might_fail, 10, 0)
if status2 then
print("Success:", error_msg2)
else
print("Failure:", error_msg2) -- Failure: SCRIPT_NAME.lua:3: division by zero
end

xpcall(func, err_handler_func):

Similar to pcall, but allows you to specify a custom error handler function (err_handler_func). If an error occurs within func, xpcall calls err_handler_func with the error object as an argument. The return values of xpcall are similar to pcall: true and results on success, or false and the result from the error handler on failure.

local function my_function_that_might_error()
-- n = n / nil -- This would cause an error: attempt to perform arithmetic on global 'n' (a nil value)
error({ code = 101, message = "A custom error occurred" }) -- Throwing a table as error object
end
local function my_error_handler(err_obj)
print("--- Custom Error Handler --- ")
if type(err_obj) == "table" then
print("Error Code:", err_obj.code)
print("Error Message:", err_obj.message)
else
print("Error:", err_obj)
end
return "Error handled gracefully" -- This value is returned by xpcall after 'false'
end
local status, result = xpcall(my_function_that_might_error, my_error_handler)
print("xpcall Status:", status)
print("xpcall Result:", result)

Expected output for the xpcall example:

--- Custom Error Handler ---
Error Code: 101
Error Message: A custom error occurred
xpcall Status: false
xpcall Result: Error handled gracefully

Best Practices:

  • Use assert for preconditions and invariants within your functions – situations that indicate a bug in your own code.
  • Use pcall or xpcall when calling external code, library functions, or any operation that might predictably fail due to external factors (e.g., file not found, network issues).
  • Provide clear and informative error messages.
  • Consider using tables as error objects for more structured error information when using error() or returning from error handlers in xpcall.