Error Handling with pcall and xpcall

· 5 min read · Updated March 17, 2026 · beginner
error-handling pcall xpcall lua-fundamentals

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

pcall always returns at least two values. The first is a boolean: true if the function ran without error, false if an error occurred. The second is either the function’s return value (if successful) or the error message (if it failed.

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:

local success, sum = pcall(function() return 1 + 2 end)
-- success = true, sum = 3

When it fails, pcall returns false followed by the error message:

local success, err = pcall(function() error("oops") end)
-- success = false, err = "oops"

Multiple return values pass through cleanly:

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.

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:

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—log it, modify it, inspect a stack trace. This is invaluable for debugging in production.

A common pattern is to get a stack traceback:

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.

When to Use pcall vs xpcall

Choose pcall when you just need to know if something worked:

-- 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:

-- 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.

Practical Examples

Wrapping File Operations

Files can fail to open, read, or write. Wrap risky operations:

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.

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

Safe Module Loading

Loading modules can fail if the module has errors:

local ok, module = pcall(require, "optional_module")

if ok then
    -- Use module safely
else
    print("Module not available, using fallback")
    -- Fallback logic
end

Common Pitfalls

Don’t forget the status check. Always check the first return value:

-- 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:

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.

Error handlers can’t prevent the error. They only handle it:

xpcall(function() error("boom") end, function(err)
    -- This runs AFTER the error
    -- Can't prevent the error, only respond to it
end)

Summary

Error handling in Lua uses protected execution to prevent crashes:

  • pcall wraps a function call and returns success/failure plus results or error
  • xpcall adds an error handler for custom processing, logging, or stack traces
  • Use pcall for simple yes/no checks
  • Use xpcall when 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 robust Lua applications that handle failures gracefully.