xpcall
xpcall calls a function with given arguments, catching any errors and passing them to a custom error handler instead of the standard traceback formatter. Unlike pcall, the error handler receives the raw error message directly.
Signature
xpcall(f, err, arg1, arg2, ...)
Parameters:
f— the function to callerr— error handler function, receives the raw error messagearg1, arg2, ...— arguments passed tof
Returns: true, result1, result2, ... on success, or false, err_msg on failure.
Basic Usage
-- Success case
local ok, result = xpcall(
function()
return math.sqrt(16)
end,
function(err)
return "Error: " .. err
end
)
-- ok = true, result = 4.0
-- Failure case
local ok, err = xpcall(
function()
error("something went wrong")
end,
function(err)
return "Caught: " .. err
end
)
-- ok = false, err = "Caught: something went wrong"
xpcall vs pcall
pcall formats the error as a string using tostring, which can lose type information:
-- pcall
local ok, err = pcall(function() error({code = 42}) end)
-- err = "table: 0x1234abc" -- loses the actual table contents
-- xpcall passes the raw error value
local ok, err = xpcall(
function() error({code = 42}) end,
function(err)
return err -- err is the actual table, not a string
end
)
-- err = {code = 42} -- preserves type and contents
Passing Arguments to the Function
Arguments after err get forwarded to the function f:
local ok, result = xpcall(
function(x, y)
return x / y
end,
function(err)
return "division failed"
end,
10, 2
)
-- ok = true, result = 5.0
Error Handler Receives the Raw Message
The key difference from pcall is that the error handler gets the raw error value, not a string:
xpcall(
function()
local data = load("return {x = 1, y = 2}")()
if data.x > 0 then
error("negative value")
end
end,
function(err)
-- err is the string "negative value", not truncated or formatted
print("Handler received:", err)
return err
end
)
Practical Example: Stack-Safe Error Logging
local function safe_call(fn, ...)
return xpcall(fn, function(err)
-- Log without re-throwing
print("[ERROR]", debug.traceback(err, 2))
return err
end, ...)
end
local result = safe_call(function(x)
if x < 0 then error("x must be positive") end
return math.sqrt(x)
end, -5)
-- Prints: [ERROR] stack traceback:
-- [C]: in function 'error'
-- ... (full traceback)
Common Patterns
Custom Error Formatting
local function format_error(err)
local trace = debug.traceback(err, 2)
return string.format("Error at %s: %s", os.date(), err)
end
local ok, result = xpcall(dangerous_function, format_error, arg1, arg2)
Conditional Error Handling
xpcall(
function()
risky_operation()
end,
function(err)
if type(err) == "table" and err.code then
-- Handle structured errors
return "Handled: " .. err.code
else
-- Handle string errors
return "Unknown error: " .. tostring(err)
end
end
)
Error Handler Return Values
The error handler’s return value becomes the second return value of xpcall:
local ok, err = xpcall(
function() error("fail") end,
function(err)
return "custom: " .. tostring(err)
end
)
-- ok = false, err = "custom: fail"
Common Pitfalls
Error Handler Itself Causing Errors
If the error handler errors, xpcall catches that too and returns false:
local ok, err = xpcall(
function() error("original") end,
function(err)
error("handler error") -- this gets caught
end
)
-- ok = false, err = "handler error"
Nil Errors
If f returns nil on error (instead of calling error), xpcall still succeeds because nil is not an error:
local ok, result = xpcall(
function()
return nil -- not an error, just a return value
end,
function(err) return err end
)
-- ok = true, result = nil
Performance
xpcall is slower than pcall due to the custom handler. Use it when you need the raw error or custom formatting, not as a general replacement.
See Also
- /reference/core-functions/ref-pcall/ — protected call without custom handler
- /reference/core-functions/ref-print/ — print values to stdout
- /tutorials/error-handling-basics/ — Lua error handling fundamentals