Error handling: error, pcall and xpcall
Raise errors with useful messages, catch them with pcall and xpcall, add tracebacks, and write cleanup that always runs.
Raising errors
-- error raises a value; if it is a string, Lua prefixes the position
local function divide(a, b)
if b == 0 then error("division by zero", 2) end -- level 2: blame the caller
return a / b
end
print(pcall(divide, 10, 0)) --> false file.lua:5: division by zero
-- level 0: no position information added
error("plain message", 0)
-- level 1 (default): blame the function that called error
-- level 2: blame the caller of that function
-- error with a table: the value is passed through untouched
local function validate(v)
if type(v) ~= "number" then
error({ code = "E_TYPE", message = "expected a number, got " .. type(v) })
end
return v
end
local ok, err = pcall(validate, "text")
print(ok, err.code, err.message) --> false E_TYPE expected a number, got string
-- assert is the terse form: it raises the message only when the value is falsey
local function openConfig(path)
local f = assert(io.open(path, "rb"), "cannot open config: " .. path)
local content = f:read("a")
f:close()
return content
end
-- a custom message loses the position unless you pass the level
local function check(n)
assert(type(n) == "number", "n must be a number") -- message only
return n
end- Any value can be an error: a string, a table, a number. A table lets you attach a code and structured detail, which a message string cannot.
errorwith a level is how library code blames the caller rather than itself. Without it, every message points at theerrorcall inside your function.assertreturns all its arguments when the first is truthy, so it composes:local f = assert(io.open(path)). It also meansassert(false, ...)with no second argument raises"assertion failed!", which tells the user nothing.- Remember that only
falseandnilare falsey.assert(0)passes.
pcall, xpcall and tracebacks
-- pcall: protected call, returns ok plus every result or the error
local ok, result = pcall(function() return 1 + 1 end)
print(ok, result) --> true 2
local ok2, err = pcall(function() error("boom") end)
print(ok2, err) --> false file.lua:5: boom
-- xpcall: run a handler before the stack unwinds, so it can capture a traceback
local function handler(err)
return {
message = tostring(err),
traceback = debug.traceback("", 2),
}
end
local ok3, info = xpcall(function()
local function inner() error("deep failure") end
inner()
end, handler)
print(ok3) --> false
print(info.message)
print(info.traceback) -- the full call stack, still intact
-- in Lua 5.1 xpcall took no arguments after the handler;
-- 5.2+ forwards extra arguments to the called function
local ok4, value = xpcall(function(a, b) return a * b end, handler, 6, 7)
print(ok4, value) --> true 42
-- a retry wrapper is a common and useful pattern
local function retry(times, f, ...)
local results = table.pack(pcall(f, ...))
local attempt = 1
while not results[1] and attempt < times do
attempt = attempt + 1
results = table.pack(pcall(f, ...))
end
return table.unpack(results, 1, results.n)
end| Function | On success | On failure |
|---|---|---|
pcall(f, ...) | true plus results | false plus the error value |
xpcall(f, h, ...) | true plus results | false plus the handler's result |
assert | Returns its arguments | Raises the message |
error | Never returns | Raises with position info by default |
debug.traceback | A string | The stack as text; useful in a handler |
Use xpcall whenever you want a traceback. By the time a plain pcall returns, the stack has unwound and debug.traceback inside the caller shows the wrong frames.
Cleanup that always runs
-- the standard pattern: catch, clean up, rethrow
local function withResource(path, body)
local f, err = io.open(path, "rb")
if not f then error(err, 2) end
local ok, result = pcall(body, f)
f:close() -- runs on both paths
if not ok then error(result, 0) end -- rethrow, keeping the original message
return result
end
withResource("data.txt", function(f)
return f:read("a")
end)
-- Lua 5.4 has to-be-closed variables: the cleanup is attached to the scope
local function withFile54(path, body)
local f <close> = assert(io.open(path, "rb"))
return body(f)
-- f:close() is called automatically here, even on an error
end
-- a function can be called at scope exit if it has a __close metamethod
local guard = setmetatable({}, {
__close = function(_, err)
print("closing, error was " .. tostring(err))
end,
})
do
local g <close> = guard
-- the guard fires when this block ends
end- Cleanup must not raise. A failing
closeinside an error path replaces the original error with a less useful one. - Rethrow with
error(err, 0)when the error value already carries position information, or it will be prefixed twice. - In Lua 5.1 and LuaJIT there are no to-be-closed variables, so the
pcallplus explicit close pattern is the only portable option. - Never use
pcallto swallow errors you do not understand. Returning a default value for a bug turns a loud failure into silent corruption.
💡
Prefer returning
nil, message for an expected failure, and raising an error for a broken precondition. Mixing the two makes every caller do both kinds of checking, and one of them will be forgotten.FAQ
Does pcall catch every error?
It catches errors raised by
error and by run-time faults inside the protected call. A C-level crash, an out-of-memory abort or an infinite loop is not catchable. The protected call also costs a little performance, so do not wrap a hot inner function.Why is my error message missing the line number?
Something called
error(msg, 0), or the message was produced by assert with a literal string, or you rethrew with level 0 after the position was already added. Check the level argument at the point where the message is created.Related
Functions, closures and multiple returns Modules, require and package.path
Last refreshed 2026-09-18.