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 baddebug.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)
endCustom 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)
endStructured 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)
endxpcall 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
endComparing 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 tracebackHandler 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
endQuick Check
What is the key advantage of xpcall over pcall?
Recap: xpcall
Summary:
xpcall(f, handler, ...)— handler runs while stack is intact- Use
debug.tracebackas 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
- The error() Function
- Protected Calls with pcall
- xpcall and Message Handlers
- Structured Error Objects