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.

The err parameter is what separates xpcall from pcall: instead of letting Lua convert the error to a string before you can inspect it, the handler gets the raw error object directly — before the stack unwinds and type information is lost. This is why structured-error patterns (passing tables with .code fields, for instance) work with xpcall and fail silently with pcall. The handler itself runs inside a protected context, so it should return a value rather than calling error() inside its own body.

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

When an error occurs inside a protected call, pcall converts the error value to a string before returning it. This conversion means you lose the original type — a table becomes an opaque address like "table: 0x1234abc", and a number becomes indistinguishable from its string representation. For most error handling this is fine, but when you need to inspect the error object itself (its type, fields, or metadata), xpcall gives you direct access through the error handler parameter. The handler receives the raw error as Lua passed it, with no conversion applied:

-- 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

After the function and error handler parameters, xpcall forwards every remaining argument directly to f. This vararg-style forwarding means you can pass any number of values — numbers, strings, tables, even nil — and they arrive at f in the same order. The error handler does not receive these arguments; it only gets the error value if f fails. This pattern is especially useful when wrapping existing functions that need parameters but don’t have built-in error handling:

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 central architectural difference between xpcall and pcall is what happens to the error value. With pcall, the error goes through tostring() before it reaches your code, stripping away any structure. With xpcall, the error handler receives exactly what was passed to error() — no conversion, no formatting, no information loss. This raw access is what makes structured error handling possible: your handler can inspect the error’s type, check for error codes, or log a detailed snapshot of the failing state. The handler can then decide whether to reformat the error, suppress it, or pass it through unchanged:

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

The following wrapper function shows xpcall in a realistic error-logging role. Instead of letting errors crash the program, it catches them, prints a full stack trace with debug.traceback(), and returns the error message so the caller can decide what to do next. The safe_call wrapper accepts any function and its arguments, making it reusable across an entire codebase. Notice how the error handler uses debug.traceback(err, 2) — the level argument of 2 skips the handler’s own stack frame, so the traceback starts from the function that actually failed:

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

The examples above cover basic usage, but real applications typically use xpcall in more structured ways. Two patterns stand out: formatting errors for consistent logging, and handling errors conditionally based on type. Both rely on the handler receiving the raw error — you can’t do either with plain pcall.

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)

For simple string errors, a format function like the one above works well. But when your code raises different kinds of errors — some structured as tables, others as plain strings — you need the handler to inspect the error before deciding how to respond. The next pattern shows a handler that distinguishes between structured errors carrying a .code field and ordinary string errors:

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

Whatever the error handler returns becomes the second return value of xpcall when the protected function fails. This is a subtle but powerful detail: it means you control what the caller sees. You can return a sanitised message, a fallback value, or even a table with diagnostic data. The handler always receives exactly one argument (the error), but it can return multiple values — though only the first return value matters to xpcall. The example below demonstrates the simplest case, where the handler prefixes the error with a label:

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

A less obvious failure mode: what happens when the error handler itself raises an error? Lua handles this gracefully — xpcall catches the handler’s error just as it caught the original one, discarding the original error entirely. The returned error message comes from the handler, not from the protected function. This means your error handler should be written defensively; a handler that calls error() risks masking the real problem. If you need the handler to signal a failure, return an error value instead of raising one:

local ok, err = xpcall(
  function() error("original") end,
  function(err)
    error("handler error")  -- this gets caught
  end
)
-- ok = false, err = "handler error"

Nil Errors

A common point of confusion: returning nil from the protected function is not the same as raising an error. Lua treats nil as an ordinary return value — xpcall reports success and passes nil through as the result. This trips up developers who assume that a nil return signals failure automatically. If your function needs to communicate an error condition, call error() explicitly. Relying on nil returns for error detection means the error handler never fires and the caller gets true, nil instead of false, error_message:

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