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.

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:

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.

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.