Error Handling: pcall and error
Reviewed & published by Brayan K
By the end of this lesson you'll raise errors with error and assert, catch them with protected calls pcall and xpcall, read the (ok, result) return convention, raise structured error objects instead of bare strings, and write cleanup that runs even on failure — because Lua has no try/catch, and pcall is the idiom.
Part of the free Lua course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
What You'll Learn
- Raise errors with error(msg, level) and guard with assert(v, msg)
- Catch errors using protected calls pcall and xpcall
- Read the (ok, result) return convention and branch on it
- Raise error objects (tables) carrying structured data
- Use the nil-plus-message convention for expected failures
- Write cleanup that runs even when an error occurs (no finally)
💡 Real-World Analogy
Think of pcall as a safety net under a trapeze. The acrobat (your risky function) performs as normal; if they slip and fall, the net catches them instead of letting them hit the floor and stopping the whole show. pcall tells you afterward whether the act succeeded (true and the result) or whether someone fell (false and what went wrong). Lua never installs the net for you automatically — there is no try/catch — so you decide exactly which acts get a net under them and which are allowed to crash the program.
1. Raising Errors: error & assert
error(msg) deliberately raises an error, stopping normal flow and propagating up until a protected call catches it; by default Lua prefixes the file:line where you called it. assert(v, msg) is the guard-clause shortcut — if v is nil or false it calls error(msg), otherwise it returns its arguments untouched. The optional level argument to error chooses whose position is blamed: 1 (default) the call site, 2 the caller, 0 no position at all. Read and run this.
-- error(msg) deliberately raises an error and stops normal flow.
-- By default Lua prefixes the file:line where error was called.
local function withdraw(balance, amount)
if amount > balance then
error("insufficient funds") -- raises; propagates up to a pcall
end
return balance - amount
end
-- assert(v, msg) is a shortcut: if v is nil/false it calls error(msg);
-- otherwise it returns v (and the rest of its arguments) unchanged.
local function openConfig(name)
assert(name, "config name is required") -- guards against a nil name
return "config:" .. name
end
print(openConfig("game")) -- prints: config:game
-- The 'level' argument controls WHO gets blamed in the message:
-- level 1 (default) -> the function that called error
-- level 2 -> that function's caller (blame the API user)
-- level 0 -> no position info added at all
local function mustBePositive(n)
if n <= 0 then
error("n must be positive", 2) -- point the finger at the caller
end
return n
end2. Catching Errors with pcall
Lua has no try/catch. Instead, pcall(fn, ...) runs fn in protected mode so an error can't escape. On success it returns true followed by fn's return values; on failure it returns false and the error value. This is the famous (ok, result) convention: capture both, then branch on ok. You can protect a named function, an inline anonymous function for a whole block of risky work, or any call that might blow up — pcall always hands control back to you.
-- Lua has NO try/catch. The idiom is a PROTECTED CALL: pcall.
-- pcall(fn, ...) runs fn; it never lets an error escape.
-- success -> returns true, <fn's return values>
-- failure -> returns false, <the error value>
local function risky(x)
-- The 0 means "no position info": without it Lua prefixes the message with
-- the file and line that raised it, which is noise in a message meant for a
-- caller rather than for you.
if x < 0 then error("negative not allowed", 0) end
return x * 2
end
-- The (ok, result) convention: capture both returns and branch on ok.
local ok, result = pcall(risky, 5)
print(ok, result) -- prints: true 10
local ok2, err = pcall(risky, -1)
print(ok2, err) -- prints: false <file>:?: negative not allowed
-- A clean pattern for "do this, but survive failure":
if ok2 then
print("got", err)
else
print("handled error:", err) -- prints: handled error: ...negative...
end
-- You can also pcall an inline function for a block of risky work:
local ok3 = pcall(function()
local t = nil
return t.field -- indexing nil raises an error
end)
print(ok3) -- prints: falseYour turn. Protect a division with pcall, read both returns, and handle a divide-by-zero. Fill in the blanks marked ___.
-- 🎯 YOUR TURN — protect a risky call and read both returns.
local function divide(a, b)
if b == 0 then error("cannot divide by zero") end
return a / b
end
-- 1) Call divide(10, 2) safely with a protected call
local ok, result = ___(divide, 10, 2) -- 👉 the protected-call function
print(ok, result) -- ✅ Expected: true 5.0
-- 2) Now try a division by zero and check the success flag
local ok2, err = pcall(divide, 10, 0)
if not ok2 then
print("error:", ___) -- 👉 the variable holding the error message
end
-- ✅ Expected output:
-- true 5.0
-- error: ... cannot divide by zero3. xpcall, Error Objects & Cleanup
xpcall is pcall with a message handler that runs at the error before the stack unwinds — pass debug.traceback to capture a full stack trace for logging. Errors don't have to be strings: error accepts any value, so you can raise a table carrying a code and message that callers inspect. And since Lua has no finally, you write cleanup by running risky work through pcall, releasing resources unconditionally afterward, then re-raising with error(result, 0) if it failed.
-- xpcall is like pcall but takes a HANDLER that runs AT the error,
-- before the stack unwinds — ideal for capturing a traceback.
local function handler(err)
return "handled: " .. tostring(err) .. "\n" .. debug.traceback()
end
local ok, info = xpcall(function()
error("boom")
end, handler)
print(ok) -- prints: false
-- 'info' now holds the message plus a full stack traceback string.
-- ERROR OBJECTS: error can carry ANY value, not just a string.
-- Raise a table to attach structured data callers can inspect.
local function parse(s)
if s == "" then
error({ code = "EMPTY", msg = "input was empty" }) -- a table error
end
return s
end
local ok2, e = pcall(parse, "")
if not ok2 then
print(e.code, e.msg) -- prints: EMPTY input was empty
end
-- CLEANUP without 'finally': run the work via pcall, then clean up,
-- then re-raise if it failed so the error isn't silently swallowed.
local function withResource(fn)
-- local res = open() -- acquire (pretend)
local ok3, result = pcall(fn) -- run the risky work
-- res:close() -- ALWAYS clean up here
if not ok3 then error(result, 0) end -- re-raise unchanged
return result
endNow you try raising errors. Validate an age with assert and error, then watch the protected calls report each outcome. Fill in the blanks:
-- 🎯 YOUR TURN — validate input with assert and error.
local function setAge(age)
-- 1) assert that age is provided (not nil) with a helpful message
___(age, "age is required") -- 👉 the guard function
-- 2) if age is negative, raise an error
if age < 0 then
___("age cannot be negative") -- 👉 the function that raises
end
return age
end
print(pcall(setAge, 25)) -- ✅ true 25
print(pcall(setAge, -5)) -- ✅ false ... age cannot be negative
print(pcall(setAge, nil)) -- ✅ false ... age is requiredPro Tips
- 💡 Two failure styles. Raise with error for truly exceptional cases; return nil, message for expected failures the caller routinely checks (like io.open).
- 💡 Guard with assert. A one-line assert(arg, "arg required") at the top of a function documents and enforces its contract.
- 💡 Use error(msg, 2) in libraries so the reported line points at the caller's mistake, not your internal code.
- 💡 xpcall + debug.traceback when you need the stack; pcall alone loses it once the stack unwinds.
Common Errors (and the fix)
- Forgetting to capture ok: writing local r = pcall(f) stores only the boolean, losing the result. Fix: capture both — local ok, r = pcall(f).
- Calling the function instead of passing it: pcall(f()) runs f unprotected first. Fix: pass the function and its args separately — pcall(f, a, b).
- Swallowing errors silently: ignoring a false from pcall hides bugs. Fix: at minimum log the error value, or re-raise with error(err) when you can't handle it.
- Double position prefix on re-raise: error(msg) adds a new file:line to an already-formatted message. Fix: re-raise with error(msg, 0) to suppress the extra prefix.
📋 Quick Reference
| Task | Code | Result / Note |
|---|---|---|
| Raise error | error("msg") | propagates up |
| Guard clause | assert(v, "msg") | errors if v falsy |
| Protected call | local ok, r = pcall(f, ...) | true,res / false,err |
| With traceback | xpcall(f, debug.traceback) | handler at error |
| Blame caller | error("msg", 2) | level 2 position |
| No position | error(value, 0) | re-raise cleanly |
| Expected failure | return nil, "msg" | caller checks |
Mini-Challenge: a safe parser wrapper
No blanks this time — just a brief and an outline. You'll wrap a risky parse in pcall and return the (ok, result) convention yourself, optionally carrying a structured error table. Build it, run it, and check your output.
-- 🎯 MINI-CHALLENGE: a safe JSON-ish parser wrapper
-- 1. Write safeParse(text) that calls a (pretend) parse(text) which may
-- raise an error. Use pcall to run it protected.
-- 2. On success, return true, value
-- On failure, return false, the error message
-- (this mirrors Lua's own (ok, result) convention).
-- 3. Bonus: make parse raise a TABLE error { code = "BAD", msg = ... }
-- and have safeParse return that code so callers can branch on it.
--
-- ✅ Example shape:
-- local ok, v = safeParse("{ ok }")
-- if ok then print("parsed", v) else print("failed:", v) end
-- your code here🎉 Lesson Complete!
- ✅ error(msg, level) raises errors; assert(v, msg) guards against falsy values
- ✅ Lua has no try/catch — pcall(fn, ...) runs code protected and returns (ok, result)
- ✅ xpcall adds a handler (e.g. debug.traceback) that runs before the stack unwinds
- ✅ Error values can be tables, letting you raise structured error objects
- ✅ No finally: run work via pcall, clean up, then re-raise with error(err, 0)
Practice quiz
Does Lua have try/catch statements?
- No, you use pcall instead
- Only in Lua 5.4
- Only with a library
- Yes, try and catch keywords
Answer: No, you use pcall instead. Lua has no try/catch; the idiom is a protected call with pcall (and xpcall).
What does pcall return when the protected function succeeds?
- Just the result
- false then the result
- true then the function's return values
- An error object
Answer: true then the function's return values. On success pcall returns true followed by whatever the function returned.
What does pcall return when the protected function raises an error?
- An empty string
- false and the error message
- true and nil
- It re-raises the error
Answer: false and the error message. On failure pcall returns false and the error value, instead of letting the error propagate.
Which function deliberately raises an error?
- fail(msg)
- throw(msg)
- raise(msg)
- error(msg)
Answer: error(msg). error(message) stops normal flow and propagates the error up to the nearest pcall.
What does assert(v, msg) do when v is nil or false?
- Calls error with msg
- Prints msg and continues
- Does nothing
- Returns v
Answer: Calls error with msg. assert raises an error using msg when its first argument is falsy; otherwise it returns its arguments.
What does the 'level' argument to error(msg, level) control?
- The output color
- Which stack frame's position is reported
- The log severity
- The retry count
Answer: Which stack frame's position is reported. level controls where the position info points: 1 is the caller of error, 2 blames the caller's caller, 0 adds no position.
How does xpcall differ from pcall?
- It is asynchronous
- It only works on coroutines
- It cannot catch errors
- It takes a message handler that runs at the point of error
Answer: It takes a message handler that runs at the point of error. xpcall takes a handler function (often to capture a traceback) invoked before the stack unwinds.
Can the error VALUE be something other than a string?
- Only numbers
- No, only strings
- Yes, it can be any value such as a table
- Only booleans
Answer: Yes, it can be any value such as a table. error accepts any value, so you can raise a structured table as an error object.
What is the common convention for functions that may fail without raising?
- Return -1
- Return nil plus an error message
- Always return true
- Print and exit
Answer: Return nil plus an error message. Many library functions return nil (or false) plus a message so callers can check without pcall, e.g. io.open.
How do you run cleanup code whether or not an error occurred?
- Run the work via pcall, then clean up afterward using the result
- Lua has a finally block
- It is impossible in Lua
- Use goto cleanup only
Answer: Run the work via pcall, then clean up afterward using the result. Without finally, you call the risky work via pcall, capture ok/result, run cleanup, then re-raise or return as needed.
Continue this course
- Previous: Modules & require
- Next: Standard Library: io, os & math — Read and write files, work with time and the system, and do math
Frequently asked questions
Does Lua have try/catch like other languages?
No. Lua deliberately has no try/catch keywords. Instead you make a protected call with pcall: you pass the risky function (and its arguments) to pcall, and it runs the function in a protected mode where an error cannot escape. pcall returns a boolean success flag followed by either the function's results or the error value. This (ok, result) pattern is the canonical Lua idiom, and once it clicks you will find it just as expressive as try/catch, with the bonus that the success flag is an ordinary value you can pass around.
What is the difference between error and assert?
error(msg) unconditionally raises an error and propagates it up to the nearest pcall, optionally taking a level argument to control which stack frame is blamed. assert(v, msg) is a convenience: it checks its first argument and, if that value is nil or false, calls error(msg) for you; if the value is truthy, assert simply returns all its arguments unchanged. So assert is perfect for guard clauses like assert(name, 'name required') at the top of a function, while error gives you full control when you need it.
What does the level argument to error do?
level decides where the position information (file and line) in the message points. The default, level 1, reports the position where error itself was called. Level 2 reports the position of the function that called the one that raised the error, which is useful in library code so the message blames the user's code rather than your internals. Level 0 disables the position prefix entirely, which is handy when you are re-raising an already-formatted message and do not want Lua to add a second location.
When should I use xpcall instead of pcall?
Use xpcall when you need to inspect the stack at the moment the error happened, because xpcall takes a message handler that runs before the stack unwinds. The classic use is passing debug.traceback as the handler so the caught error includes a full traceback for logging. pcall, by contrast, only hands you the error value after the stack has already collapsed, so a traceback gathered afterward would be useless. For simple catch-and-branch logic pcall is enough; reach for xpcall when diagnostics matter.
How do I run cleanup code since Lua has no finally?
You structure it manually. Acquire your resource, then run the risky work through pcall so an error cannot skip past your cleanup. Capture the (ok, result) pair, perform the cleanup unconditionally on the next lines, and then decide what to do: if ok is false, re-raise with error(result, 0) so the failure is not silently swallowed, otherwise return the result. Because pcall guarantees control returns to you whether the work succeeded or failed, this gives you the same safety a finally block would.