0Pricing
Lua Academy · Lesson

Protected Calls with pcall

Catch errors safely using pcall and interpret return values.

Protected Calls with pcall is a free Lua Academy lesson on CoddyKit — lesson 2 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the Lua Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

pcall Basics

pcall(f, ...) calls function f with arguments ... in protected mode. If f succeeds, pcall returns true followed by all return values. If f errors, pcall returns false followed by the error message. The program continues either way.

local function risky(x)
  if x < 0 then error("negative input: " .. x) end
  return math.sqrt(x)
end

local ok, result = pcall(risky, 9)
print(ok, result)    -- true  3.0

ok, result = pcall(risky, -1)
print(ok, result)    -- false  ...: negative input: -1

Catching Specific Errors

Check the error value type after pcall to handle different error kinds differently. Table errors have structured data; string errors have a message. Always handle both cases.

local function op(errType)
  if errType == "table" then
    error({code=42, msg="structured error"})
  else
    error("plain string error")
  end
end

local ok, err = pcall(op, "table")
if not ok then
  if type(err) == "table" then
    print("Code:", err.code, "Msg:", err.msg)
  else
    print("String error:", err)
  end
end

Multiple Return Values on Success

On success, pcall returns all of the protected function's return values after the initial true. Capture them with multiple variables.

local function compute(a, b)
  return a + b, a * b, a - b
end

local ok, sum, product, diff = pcall(compute, 6, 4)
if ok then
  print("sum:", sum, "product:", product, "diff:", diff)
  -- sum: 10  product: 24  diff: 2
end

Nesting pcall

pcall calls can be nested. Each pcall creates an independent protected scope. An error in an inner pcall is caught by the inner pcall, not the outer one. This lets you implement fallback strategies.

local function tryPrimary()
  error("primary failed")
end

local function tryFallback()
  return "fallback result"
end

local ok, result = pcall(tryPrimary)
if not ok then
  print("Primary failed:", result)
  ok, result = pcall(tryFallback)
end

if ok then print("Got:", result) end  -- fallback result

pcall with Methods

To call a method (colon syntax) via pcall, pass the function and the object as the first argument. pcall(obj.method, obj, args...) or wrap in a closure.

local obj = {
  x = 10,
  compute = function(self, n)
    if n == 0 then error("zero divisor") end
    return self.x / n
  end
}

-- Pass method + self explicitly
local ok, v = pcall(obj.compute, obj, 2)
print(ok, v)   -- true  5.0

-- Or use a closure
ok, v = pcall(function() return obj:compute(0) end)
print(ok, v)   -- false  ...zero divisor

pcall for IO Safety

Wrap IO operations in pcall to catch unexpected OS errors. Return a consistent result/error pair so callers can handle failures gracefully.

local function safeRead(path)
  local ok, result = pcall(function()
    local f = assert(io.open(path, "r"))
    local content = f:read("a")
    f:close()
    return content
  end)
  if ok then return result
  else return nil, result
  end
end

local data, err = safeRead("config.txt")
if data then print("Read", #data, "bytes")
else print("Error:", err)
end

Retry Pattern with pcall

Retry a flaky operation (network request, file write) up to N times, using pcall to catch errors. Sleep between retries in real code (or use coroutines).

local function retry(fn, maxAttempts)
  local attempts = 0
  while attempts < maxAttempts do
    attempts = attempts + 1
    local ok, result = pcall(fn)
    if ok then
      print("Succeeded on attempt", attempts)
      return result
    end
    print("Attempt", attempts, "failed:", result)
  end
  error("all " .. maxAttempts .. " attempts failed")
end

local n = 0
retry(function()
  n = n + 1
  if n < 3 then error("not ready") end
  return "done"
end, 5)

Capturing pcall in Table

When pcall is used in a loop, store results in a table for later analysis. This is useful for batch operations where you want to process all items and collect errors rather than stopping at the first failure.

local jobs = {10, -5, 25, 0, 16}
local results = {}

for _, n in ipairs(jobs) do
  local ok, v = pcall(function()
    if n < 0 then error("negative") end
    return math.sqrt(n)
  end)
  results[#results+1] = {input=n, ok=ok, value=v}
end

for _, r in ipairs(results) do
  if r.ok then
    print(r.input, "->", string.format("%.3f", r.value))
  else
    print(r.input, "-> ERROR:", r.value)
  end
end

pcall vs xpcall

pcall catches the error but gives you only the error value. xpcall lets you provide a message handler function that runs while the stack is still intact, enabling stack traces. Choose xpcall when you need tracebacks.

-- pcall: simple, no traceback
local ok1, err1 = pcall(function()
  error("simple error")
end)
print(err1)   -- only the error message

-- xpcall: with traceback handler
local ok2, err2 = xpcall(
  function() error("traced error") end,
  function(e) return debug.traceback(e, 2) end
)
print(err2)   -- full stack trace

pcall Return Convention

Use pcall as the basis for a consistent API: always return value on success or nil, errorMessage on failure. Layer your functions this way so callers always know what to expect.

local function loadJSON(path)
  local ok, result = pcall(function()
    local f = assert(io.open(path, "r"))
    local text = f:read("a")
    f:close()
    -- Fake JSON parse for demo
    return {data = text, path = path}
  end)
  if ok then return result
  else return nil, result
  end
end

local data, err = loadJSON("data.json")
if data then print("Loaded:", data.path)
else print("Failed:", err)
end

Quick Check

What does pcall return when the protected function succeeds?

Recap: pcall

Summary:

  • pcall(f, ...)true, results... or false, err
  • Check first return to determine success/failure
  • Multiple return values after true on success
  • Nested pcall: each catches its own errors independently
  • Retry pattern: loop with pcall for flaky operations
  • Use xpcall when you need stack traces

Frequently asked questions

Is the “Protected Calls with pcall” lesson free?

Yes — the full text of “Protected Calls with pcall” is free to read here on the web, and the Lua Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the Lua Academy course, upgrade to CoddyKit PRO.

What will I learn in “Protected Calls with pcall”?

Catch errors safely using pcall and interpret return values. You practise Lua Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start Lua Academy?

No prior experience is required. Lua Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 2 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Protected Calls with pcall” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this Lua Academy lesson?

Yes. Every Lua Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. The error() Function
  2. Protected Calls with pcall
  3. xpcall and Message Handlers
  4. Structured Error Objects
← Back to Lua Academy