Error handling
Lesson 8.1
Things go wrong: a value is missing, a player types nonsense, a file is broken. Lua lets you raise errors on purpose, catch them, and clean up after them.
What an error does
When an error happens, Lua stops the current code and goes back up through the function calls until something catches it. If nothing does, the program stops and prints the message:
-- this breaks on purpose
local player = nil
print(player.name)
input:3: attempt to index a nil value (local 'player')
In a Celestial mod, the game catches errors from your event handlers, logs them with the line number, and keeps running. A mod that keeps failing gets turned off for the session, so it is still worth handling errors properly.
Raising an error: error
error(message) raises an error yourself. Use it when your function gets input it can’t work with:
local function set_health(player, value)
if type(value) ~= "number" then
error("health must be a number, got " .. type(value))
end
player.health = value
end
Lua adds the file and line to the message. The second argument, the level, decides which line that is: 1 (the default) points at the error call, 2 points at the code that called your function, which is usually where the real mistake is. 0 adds no position at all.
local function need_number(x)
if type(x) ~= "number" then
error("expected a number", 2) -- blame the caller
end
return x
end
assert
assert(value, message) raises an error when value is false or nil, and otherwise returns its arguments. It is a short way to check something that should always be true:
local function divide(a, b)
assert(b ~= 0, "can't divide by zero")
return a / b
end
print(divide(10, 4)) --> 2.5
Many functions return nil plus a message when they fail. assert turns that into an error in one step: local n = assert(tonumber(text), "not a number").
Catching errors: pcall
pcall(f, ...) calls f in protected mode. If f works, pcall returns true and f’s results. If f raises an error, pcall returns false and the error message, and your program keeps going:
local ok, result = pcall(function()
return 10 // 2
end)
print(ok, result) --> true 5
local ok2, err = pcall(function()
error("not enough gold", 0)
end)
print(ok2, err) --> false not enough gold
You can pass arguments straight through instead of wrapping in a new function:
local ok, n = pcall(math.floor, 3.7)
print(ok, n) --> true 3
Error values don’t have to be strings
error accepts any value, including a table. That is handy when the caller needs to know which kind of error happened:
local function buy(gold, price)
if gold < price then
error({ code = "too_poor", missing = price - gold })
end
return gold - price
end
local ok, err = pcall(buy, 5, 12)
if not ok and err.code == "too_poor" then
print("You need " .. err.missing .. " more gold.") --> You need 7 more gold.
end
Lua adds no position to table errors. The Celestial API raises errors like this: they have a code and a message field.
xpcall and tracebacks
xpcall(f, handler, ...) is pcall with a message handler. The handler runs at the moment of the error, before the call stack is unwound, so it can record where the error came from. debug.traceback is the usual handler: it adds the list of calls that led to the error.
local function inner() error("something broke") end
local function outer() inner() end
local ok, trace = xpcall(outer, debug.traceback)
print(ok) --> false
print(trace)
The trace starts with the message, which already says where the error happened. Below it, stack traceback: lists the calls that led there, newest first: error, then inner, then outer, each with its line.
Expected failures: return nil, message
Not every problem should be an error. For things that are a normal part of using a function, such as “that player isn’t online”, it is often nicer to return nil and a message, the way tonumber and many library functions do:
local players = { alex = { health = 20 } }
local function find_player(name)
local p = players[name:lower()]
if not p then
return nil, "no player called " .. name
end
return p
end
local p, why = find_player("Steve")
if not p then
print(why) --> no player called Steve
end
A rough rule: raise an error for bugs (wrong argument types, broken state) and return nil, message for situations the caller should handle.
Cleaning up with close
Lua 5.4 has to-be-closed variables. Mark a local with <close> and its value’s __close metamethod runs when the variable goes out of scope, whether the block ends normally, with return, break, or an error:
local function lock(name)
print("lock " .. name)
return setmetatable({}, {
__close = function()
print("unlock " .. name)
end,
})
end
do
local guard <close> = lock("chest")
print("using the chest")
end
lock chest
using the chest
unlock chest
It works with pcall too: if the block fails, the value is still closed before the error goes further. The Celestial API uses this for things such as world snapshots that should be released straight away.
Try it
Write parse_amount(text) that turns a string like "16" into an integer between 1 and 64. Raise an error (with level 2) when the text isn’t a number, isn’t whole, or is out of range. Then call it with "16", "abc", "2.5" and "100" inside pcall and print the results.
Show a solution
local function parse_amount(text)
local n = tonumber(text)
if not n then
error("not a number: " .. text, 2)
end
n = math.tointeger(n)
if not n then
error("must be a whole number: " .. text, 2)
end
if n < 1 or n > 64 then
error("must be between 1 and 64: " .. text, 2)
end
return n
end
for _, text in ipairs({ "16", "abc", "2.5", "100" }) do
print(pcall(parse_amount, text))
end
The first call prints true 16. The others print false and their message. When pcall calls the function directly, there is no calling line to blame, so the messages have no position.
Recap: error and assert raise errors, pcall and xpcall catch them, and <close> variables clean up even when something fails.