Error Handling: pcall and error
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.
Learn Error Handling: pcall and error in our free Lua course — an interactive lesson with worked examples, a practice exercise and a quick reference.
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
💡 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.
2. 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.
Your turn. Protect a division with pcall , read both returns, and handle a divide-by-zero. Fill in the blanks marked ___ .
3. 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.
Now you try raising errors. Validate an age with assert and error , then watch the protected calls report each outcome. Fill in the blanks:
Pro 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
Frequently Asked Questions
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.
🎉 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)
- ✅ Next lesson: Standard Library: io, os & math — files, time, and numbers
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