Errors#

Lua has no exception keyword. Errors are raised with error and caught with pcall (protected call). A call that runs outside pcall propagates the error up the stack and prints a message when it reaches the top.

/controls`. For the assert family used heavily in tests, see Testing.

The model#

  • error(msg) raises. msg is conventionally a string; any value works.

  • pcall(f, ...) calls f in protected mode and returns true, ... on success or false, msg on error. The stack does not unwind past the pcall.

  • xpcall(f, handler, ...) is pcall plus a handler that runs with the original stack still intact, so debug.traceback returns something useful.

  • assert(v, msg) is if not v then error(msg or "assertion failed") end, the standard guard at the top of a function.

local function parse(s)
  local n = tonumber(s)
  if not n then error("not a number: " .. s, 2) end
  return n
end

local ok, result = pcall(parse, "x")
if not ok then
  io.stderr:write("parse failed: ", result, "\n")
end

The second argument to error is the level at which to report the error. Level 1 (default) blames the line that called error; level 2 blames the caller of the function containing error. Operators use level 2 for input-validation errors so the message points at the bad call site.

Returning, not raising#

Lua’s stdlib convention is to return nil plus an error string on recoverable failure, and to raise only for programming mistakes. io.open, os.execute, and most network calls follow this.

local f, err = io.open("/etc/shadow", "r")
if not f then
  io.stderr:write("open failed: ", err, "\n")
  return
end

Follow the same convention in their own code: return value on success, nil, "message" on a failure the caller can plausibly recover from.

pcall#

pcall runs its first argument in protected mode. The call returns true plus whatever the function returned on success, or false plus the error value on failure.

local ok, a, b = pcall(function() return 1, 2 end)
--> ok = true, a = 1, b = 2

local ok, err = pcall(function() error("boom") end)
--> ok = false, err = "input:1: boom"

The error value carries Lua’s standard file:line: msg prefix when it was raised with a string. Strip it manually if the message is going to a user-facing surface.

xpcall#

xpcall is pcall with a handler that runs before the stack unwinds. The standard handler is debug.traceback so the caller gets a stack trace instead of a one-line message.

local function run()
  deep()
end

local ok, err = xpcall(run, debug.traceback)
if not ok then
  io.stderr:write(err, "\n")
end

In long-running services (OpenResty, Wireshark dissectors, Neovim plugins) wrap every entry point in xpcall so a bug in user code does not crash the host.

Re-raising#

Re-raise by calling error again with the captured value.

local ok, err = pcall(do_thing)
if not ok then
  log.warn("retrying after: " .. tostring(err))
  -- ...
  error(err, 0)            -- 0 means "use msg verbatim"
end

Level 0 keeps the original file:line: prefix that was baked into the error message, so the re-raise looks like the original failure site.

assert#

assert(v, msg) is the operator’s terse guard. If v is truthy, the call returns v (and any extra args), so the operator can chain it inline.

local f = assert(io.open(path, "r"), "cannot open " .. path)
local n = assert(tonumber(arg[1]),    "expected a number")

assert raises with "assertion failed!" if no message is supplied. The operator always supplies a message.

References#