Error Handling with pcall and xpcall
Prerequisites
This tutorial assumes you’re comfortable with basic Lua syntax — functions, variables, and control flow. All examples use Lua 5.1 or later. The debug.traceback function used in the xpcall section requires the debug library to be available (it is in the standard distribution).
Why error handling matters
In Lua, errors are inevitable. A file might not exist, a network call might fail, or a user might pass invalid input. Without error handling, any error crashes your entire program. That’s where pcall and xpcall come in; they let you catch and handle errors gracefully instead of letting them propagate.
The problem with regular function calls
When you call a function that errors, Lua doesn’t return to your code; it throws the error up the call stack:
function divide(a, b)
return a / b -- This will error if b is 0
end
divide(10, 0) -- stdin:1: attempt to divide by zero
The program crashes. In a long-running server or game, this is unacceptable. You need a way to attempt an operation and handle failures without bringing down everything else.
Protected execution with pcall
pcall stands for “protected call.” It runs a function in protected mode; instead of erroring, it returns a status code telling you whether the call succeeded:
local success, result = pcall(divide, 10, 0)
if success then
print("Result:", result)
else
print("Error:", result) -- "attempt to divide by zero"
end
The first return value from pcall is always a boolean that tells you whether the call succeeded. You must check this flag before using any of the other return values, because the meaning of those values changes depending on success or failure. A true first value means the remaining values are the function’s normal return values; a false first value means the second return value is the error message. This is the single most important rule when working with protected calls. Beyond the basics, pcall also accepts extra arguments and passes them directly to the target function, which makes it convenient for wrapping functions that take parameters. This pattern lets you write defensive code:
local function read_config(filename)
local file = io.open(filename, "r")
if not file then
return nil, "Could not open file"
end
local content = file:read("*a")
file:close()
return content
end
local success, result = pcall(read_config, "config.txt")
if success then
print("Config loaded:", result)
else
print("Failed to load config:", result)
end
Understanding pcall’s return values
When the protected function succeeds, pcall returns true followed by whatever the function returns. This means you can chain pcall with functions that produce any number of outputs without losing data. The success path is straightforward: the first value confirms the call worked, and everything after that belongs to your function.
local success, sum = pcall(function() return 1 + 2 end)
-- success = true, sum = 3
When it fails, pcall returns false followed by the error message. The error message is whatever the failing function produced, either from error() calls, assertion failures, or Lua’s built-in runtime errors like division by zero. Knowing that the second value switches from “function result” to “error string” is essential for writing correct error handling logic.
local success, err = pcall(function() error("oops") end)
-- success = false, err = "oops"
Multiple return values pass through cleanly. This is one of pcall’s best features: it preserves Lua’s multi-return semantics transparently. You don’t need to pack return values into a table or restructure your functions to work with protected calls. The caller receives exactly what the original function would have returned, with the boolean status flag prepended.
local function stats()
return 10, 20, 30
end
local ok, a, b, c = pcall(stats)
-- ok = true, a = 10, b = 20, c = 30
This makes pcall flexible for any function signature without requiring wrapper code or result packing. The ability to preserve multiple return values is what lets you use pcall as a drop-in safety layer around existing functions without changing how they communicate results.
Error handling with xpcall
xpcall is similar to pcall but adds a second argument: an error handler function. Instead of getting the raw error message, you can process it through your handler first. The error handler runs after the error has already occurred; it cannot prevent the error or rescue the execution, but it can transform, log, or enrich the error message before it reaches the caller.
local function my_error_handler(err)
print("Caught error:", err)
return "Handled: " .. err -- Can return a modified message
end
local success, result = xpcall(function() error("fail!") end, my_error_handler)
-- Prints: Caught error: fail!
-- success = false, result = "Handled: fail!"
The error handler receives the error message and can do anything with it: log it to a file, send it to a monitoring service, or wrap it with additional context like a timestamp or the current function name. This is invaluable for debugging in production environments where raw error strings don’t provide enough context. A common pattern is to capture a stack traceback alongside the error message, which tells you what went wrong and where in the call chain the failure originated.
local function debug_handler(err)
local trace = debug.traceback("", 2) -- Get stack trace
return err .. "\n" .. trace
end
xpcall(dangerous_function, debug_handler)
The debug.traceback call captures where the error actually occurred, not just the error message. The second argument to traceback sets the stack level, and passing 2 skips the handler itself so the trace points directly to the failing code. This approach gives you full call-stack visibility in your error reports.
When to use pcall vs xpcall
Choose pcall when you just need to know if something worked. This covers the majority of error handling scenarios: parsing data, loading optional dependencies, or calling functions where failure is expected and the raw error message is sufficient for logging or display.
-- Simple yes/no check
local ok, data = pcall(json.decode, input_string)
if not ok then
return nil -- Silent failure is fine here
end
Choose xpcall when you need to handle the error with custom logic. Situations that call for xpcall include production logging pipelines, metrics collection, stack trace enrichment, or any scenario where the raw error string needs transformation before the caller sees it. The error handler gives you a single place to enforce consistent error formatting across your entire codebase.
-- Need to log or transform the error
xpcall(api_call, function(err)
logger:error("API call failed: " .. err)
metrics:increment("api.failures")
end)
In practice, pcall covers most cases. Reach for xpcall when your error handler needs to do more than just check success or failure. The extra ceremony of defining an error handler function is only justified when you have a specific need for transformed errors, centralized logging, or stack traces.
Practical examples
Wrapping file operations
Files can fail to open, read, or write for many reasons: missing paths, permission denials, disk-full conditions, or concurrent access conflicts. Wrapping risky file operations in pcall ensures that a single bad file doesn’t crash your entire program. The pattern is to place the full file workflow inside the protected function so that every step is guarded.
local function safe_read(path)
local ok, result = pcall(function()
local f = assert(io.open(path, "r")) -- assert still throws!
local content = f:read("*a")
f:close()
return content
end)
if ok then
return result
else
return nil, result
end
end
Note the assert inside the protected call: it will still trigger the error handler because pcall catches all Lua errors, including those raised by assert. This is actually a useful property because it means you can use assert freely inside protected functions to validate conditions and let pcall catch the failures uniformly. The assert calls serve as inline guards that short-circuit execution and produce clear error messages, while pcall converts those into clean return values.
Validating user input
local function parse_age(input)
local ok, age = pcall(function()
local n = tonumber(input)
assert(n, "Not a number")
assert(n > 0, "Must be positive")
assert(n < 150, "Implausible age")
return n
end)
if ok then
return age
else
return nil, "Invalid age: " .. age
end
end
This example shows how multiple assert calls inside a single pcall create a validation pipeline. Each assertion tests one rule, and if any rule fails, execution stops immediately and pcall returns the specific error message. The caller doesn’t need to know which assertion fired; it just sees a clean success or a descriptive failure reason. This pattern combines validation with error handling in a remarkably concise way.
Safe module loading
Loading modules can fail if the module has errors, if the file is missing from the search path, or if there are circular dependencies. Using pcall with require gives you a clean fallback path when an optional dependency isn’t available.
local ok, module = pcall(require, "optional_module")
if ok then
-- Use module safely
else
print("Module not available, using fallback")
-- Fallback logic
end
This approach is especially useful for plugins, optional features, or libraries that may not be installed in every environment. Rather than crashing on a missing require, your program detects the absence and activates alternative behavior. The same technique works for loading local modules that might be under development or conditionally included.
Common pitfalls
Don’t forget the status check. Always check the first return value. Skipping it means you’ll treat error messages as valid results or valid results as error conditions. The boolean flag is not optional; it is the contract that pcall and xpcall guarantee.
-- Wrong
local result = pcall(dangerous) -- Misses the success flag!
-- Right
local ok, result = pcall(dangerous)
Remember pcall catches all errors. This includes errors you might want to propagate. If your function calls error() intentionally to signal an unrecoverable condition, pcall will catch it just like any other error. You need to be deliberate about which errors should be caught and which should surface to the caller.
local ok, err = pcall(function()
if something_wrong then
error("Intentional abort") -- Caught by pcall
end
end)
If you need to distinguish between recoverable and unrecoverable errors, use a custom error mechanism instead of Lua errors. One approach is to return structured error tables that callers can inspect. Another is to use a sentinel value convention where certain error patterns indicate “let this one through.”
Error handlers can’t prevent the error. They only handle it after the fact. The protected function has already stopped executing by the time your handler runs, so there is no way to recover and continue from the point of failure inside the same protected call.
xpcall(function() error("boom") end, function(err)
-- This runs AFTER the error
-- Can't prevent the error, only respond to it
end)
This is a common misunderstanding for developers coming from languages with try/catch where you can sometimes resume execution. In Lua, the handler is purely for reporting and transformation; the protected function’s execution is over the moment an error is raised.
Next steps
Error handling with pcall and xpcall gives you the foundation for dependable Lua programs. The next tutorial in this series explores file I/O, where protected calls become essential — every file operation can fail, and handling those failures gracefully is what separates production code from throwaway scripts.
Summary
Error handling in Lua uses protected execution to prevent crashes:
pcallwraps a function call and returns success/failure plus results or errorxpcalladds an error handler for custom processing, logging, or stack traces- Use
pcallfor simple yes/no checks - Use
xpcallwhen you need to do something with the error - Always check the first return value; never ignore it
These two functions are your foundation for writing dependable Lua applications that handle failures gracefully.
See also
- /tutorials/lua-fundamentals/file-io/ — reading and writing files in Lua
- /tutorials/lua-fundamentals/modules-and-require/ — organizing code with modules and require
- /reference/core-functions/ref-pcall/ — pcall API reference