Skip to content
AgileLogics
6 min read

Error Handling

Prerequisites: Functions

Raising errors

local function withdraw(balance, amount)
  if type(amount) ~= "number" then
    error("amount must be a number", 2)
  end
  if amount > balance then
    error({code = "INSUFFICIENT", needed = amount - balance})
  end
  return balance - amount
end
  • error(message) stops the current function and unwinds the stack until something catches it.
  • The optional level (2) blames the caller's line in the message, which is more useful for argument errors.
  • The error value can be any value — a table carries structured details.

Catching errors with pcall

local function withdraw(balance, amount)
  if type(amount) ~= "number" then error("amount must be a number", 2) end
  if amount > balance then error({code = "INSUFFICIENT", needed = amount - balance}) end
  return balance - amount
end

print(pcall(withdraw, 100, 30))

local ok, err = pcall(withdraw, 100, "ten")
print(ok, err)

ok, err = pcall(withdraw, 100, 250)
print(ok, type(err), err.code, err.needed)
true	70
false	amount must be a number
false	table	INSUFFICIENT	150

pcall(f, args...) ("protected call") returns true plus the results, or false plus the error value — it never lets the error escape.

xpcall and tracebacks

local function inner() error("something broke") end
local function outer() inner() end

local ok, msg = xpcall(outer, function(err)
  return "handled: " .. err
end)
print(ok, msg)

local _, trace = xpcall(outer, debug.traceback)
print(trace:match("^[^\n]+"))
false	handled: main.lua:1: something broke
main.lua:1: something broke

xpcall calls a message handler while the stack is still intact — passing debug.traceback captures the full stack trace for logging.

assert

local function parseAge(s)
  local n = assert(tonumber(s), "not a number: " .. tostring(s))
  return n
end
print(parseAge("36"))
print(pcall(parseAge, "abc"))
36
false	main.lua:2: not a number: abc

assert(value, message) returns value if it's truthy, and raises the message otherwise. It pairs well with functions that return nil, "error message":

local f, err = io.open("missing.txt")
print(f, err)
print(pcall(function() return assert(io.open("missing.txt")) end))
nil	missing.txt: No such file or directory
false	main.lua:3: missing.txt: No such file or directory

Two error styles

  • Return nil, message for expected failures the caller should handle (a missing file, invalid input). The standard library uses this style.
  • Raise with error for programming mistakes and truly exceptional situations.