0Pricing
Lua Academy · Lesson

xpcall and Message Handlers

Use xpcall with a custom handler for detailed error tracebacks.

xpcall and Message Handlers is a free Lua Academy lesson on CoddyKit — lesson 3 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Lua Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

xpcall Syntax

xpcall(f, handler, ...) calls f with arguments ... in protected mode, and on error calls handler(errorObject). The handler's return value becomes the second return value of xpcall. Unlike pcall, xpcall runs the handler while the stack is intact.

local function handler(err)
  return "HANDLED: " .. tostring(err)
end

local ok, msg = xpcall(
  function() error("something bad") end,
  handler
)

print(ok)   -- false
print(msg)  -- HANDLED: ...: something bad

debug.traceback as Handler

The most common xpcall handler is debug.traceback. Pass it directly as the handler — it formats the error with a full call stack trace, invaluable for debugging production errors.

local function level3() error("deep error") end
local function level2() level3() end
local function level1() level2() end

local ok, err = xpcall(level1, debug.traceback)

if not ok then
  -- err contains the full stack trace
  print(err)
end

Custom Handler with Context

A custom handler can add context: timestamp, log to file, notify monitoring systems, then return the formatted error. This is the standard pattern for production error handling in Lua applications.

local function errorHandler(err)
  local trace = debug.traceback(err, 2)
  local ts = os.date("%H:%M:%S")
  local report = string.format("[%s] ERROR\n%s", ts, trace)
  -- Could log to file here
  io.stderr:write(report .. "\n")
  return report
end

local ok, msg = xpcall(
  function()
    local t = nil
    return t.field   -- error!
  end,
  errorHandler
)
print("ok:", ok)

Handler Cannot Error

If the message handler itself raises an error, Lua returns an error indicator without calling the handler again. Always write handlers that are bulletproof — no IO that can fail, no nil indexing.

local function safeHandler(err)
  -- Keep handler simple and safe
  local ok, trace = pcall(debug.traceback, err, 2)
  if ok then return trace
  else return tostring(err) .. " (traceback failed)"
  end
end

local ok, msg = xpcall(
  function() error({complex="error table"}) end,
  safeHandler
)
print(ok, type(msg))

xpcall for Main Loop

In long-running programs (servers, game loops), wrap the main function in xpcall to catch and log any unhandled errors without crashing. The main loop can then decide whether to restart or exit.

local function mainApp()
  -- simulate work
  for i = 1, 3 do
    print("Tick", i)
    if i == 2 then error("transient error") end
  end
end

local function handler(e)
  return debug.traceback("App error: "..tostring(e), 2)
end

local ok, err = xpcall(mainApp, handler)
if not ok then
  print("Application crashed:\n" .. err)
end

Structured Error Reporting

Combine xpcall with structured error objects and a rich handler to produce detailed, actionable error reports for debugging or monitoring dashboards.

local function handler(err)
  local info = {
    error   = tostring(err),
    time    = os.date("!%Y-%m-%dT%H:%M:%SZ"),
    trace   = debug.traceback(nil, 2),
  }
  return info
end

local ok, report = xpcall(
  function() error({code=500, msg="internal error"}) end,
  handler
)

if not ok then
  print("Time:", report.time)
  print("Error:", report.error)
  -- print("Trace:", report.trace)
end

xpcall for Coroutines

Inside a coroutine, pcall works normally. To get tracebacks from errors inside coroutines, wrap the coroutine body in xpcall. The handler runs inside the coroutine's stack context.

local function co_body()
  error("error inside coroutine")
end

local co = coroutine.create(function()
  local ok, err = xpcall(co_body, debug.traceback)
  if not ok then
    print("Caught in coroutine:", err:match("([^\n]+)"))
  end
end)

coroutine.resume(co)

Error Object Enrichment

The handler can enrich a plain string error into a rich object, or a rich object further. This lets lower-level code throw simple errors while the handler adds context (request ID, user session, environment info).

local requestID = "req-123"

local function handler(err)
  if type(err) == "string" then
    return {message=err, requestID=requestID, level="error"}
  end
  err.requestID = requestID
  return err
end

local ok, result = xpcall(
  function() error("database timeout") end,
  handler
)

if not ok then
  print(result.message, result.requestID)
  -- database timeout  req-123
end

Comparing pcall and xpcall

Use pcall when: you only need the error value, the error is expected and handled inline, simplicity matters. Use xpcall when: you need stack traces, you're at a top-level boundary, you want to add context to all errors.

-- pcall: simple, no overhead
local ok, err = pcall(function()
  return 1/0   -- no error in Lua! returns inf
end)
print(ok, err)   -- true  inf

-- xpcall: adds traceback
local ok2, err2 = xpcall(
  function() error("real error") end,
  debug.traceback
)
print(ok2)       -- false
print(err2:sub(1,40))  -- first line of traceback

Handler Return Value

Whatever the handler returns becomes the second value from xpcall. If the handler returns nil, xpcall's second return is nil. A handler that returns the original error object plus extra info is the most flexible approach.

local function enrichedHandler(err)
  return {
    original = err,
    traceback = debug.traceback(nil, 2),
    timestamp = os.time(),
  }
end

local ok, report = xpcall(
  function() error("oops") end,
  enrichedHandler
)

if not ok then
  print(type(report))          -- table
  print(report.original)       -- ...: oops
  print(report.timestamp > 0)  -- true
end

Quick Check

What is the key advantage of xpcall over pcall?

Recap: xpcall

Summary:

  • xpcall(f, handler, ...) — handler runs while stack is intact
  • Use debug.traceback as handler for full tracebacks
  • Handler must not error — keep it simple
  • Enrich errors with context in the handler
  • Wrap main loops / server handlers in xpcall
  • Handler return becomes xpcall's second return value

Frequently asked questions

Is the “xpcall and Message Handlers” lesson free?

Yes — the full text of “xpcall and Message Handlers” is free to read here on the web, and the Lua Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Lua Academy course, upgrade to CoddyKit PRO.

What will I learn in “xpcall and Message Handlers”?

Use xpcall with a custom handler for detailed error tracebacks. You practise Lua Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start Lua Academy?

No prior experience is required. Lua Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 3 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “xpcall and Message Handlers” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this Lua Academy lesson?

Yes. Every Lua Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. The error() Function
  2. Protected Calls with pcall
  3. xpcall and Message Handlers
  4. Structured Error Objects
← Back to Lua Academy