luaguides

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 call
  • err — error handler function, receives the raw error message
  • arg1, arg2, ... — arguments passed to f

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