lus

Acquis 1 - Catch

Contents (10)
  1. Syntax
  2. Return values
  3. Function calls
  4. Composition with other Lus features
    1. With if-assignment
  5. Motivation
    1. The anonymous function problem
    2. Dependency on the global library
    3. Verbose error checking patterns
    4. Testing error conditions

The catch expression evaluates any expression in protected mode and returns a boolean success flag alongside the result of the expression or an error message.

Syntax#

The catch keyword is followed by any expression:

local success,  ... = catch <expression>

When the expression evaluates successfully, success is true and ... contains the value of the expression. When the expression raises an error, success is false and ... is instead the error message.

-- Successful evaluation
local ok, val = catch 1 + 2
assert(ok == true)
assert(val == 3)

-- Error during evaluation
local ok, err = catch 1 + "a"
assert(ok == false)
assert(type(err) == "string")  -- error message

Return values#

The catch expression always returns at least a boolean status, and either the results or error message. When you only need to know whether an expression succeeded, you can ignore the second value:

local ok = catch someRiskyOperation()
if not ok then
  -- handle error without needing the message
end

This pattern is particularly useful for assertions when testing error conditions:

-- Assert that an operation fails
assert(not catch type())           -- type() with no args errors
assert(not catch coroutine.yield()) -- yield outside coroutine errors

Function calls#

The most common use case is catching errors from function calls:

local ok, result = catch require("no-such-module")
if not ok then
  print("Module not found: " .. result)
end

local ok, data = catch load(someCode)
if ok then
  data()  -- execute the loaded chunk
end

Composition with other Lus features#

With if-assignment#

The catch expression integrates naturally with Lus’s if-assignment syntax:

if ok, val = catch someOperation() then
  print("Success:", val)
else
  print("Failed:", val)  -- val contains error message here
end

Motivation#

Lua’s error handling primitives, pcall and xpcall, operate at the function level. This design has several limitations that become apparent in real-world code.

The anonymous function problem#

To protect a single expression, Lua requires wrapping it in an anonymous function:

-- Lua: protecting a single operation
local ok, result = pcall(function()
  return someValue + otherValue
end)

This boilerplate obscures the intent and adds cognitive overhead. With catch, the same operation is direct:

-- Lus: direct expression protection
local ok, result = catch someValue + otherValue

Dependency on the global library#

pcall and xpcall are global functions, meaning code that uses protected execution depends on the global environment. Sandboxed or restricted environments may not expose these functions, leaving code unable to handle errors gracefully.

The catch keyword is a language construct, not a library function. It works in any environment regardless of what globals are available.

Verbose error checking patterns#

Common Lua patterns for error checking become verbose:

-- Lua: checking if an operation would fail
local ok = pcall(function() type() end)
if not ok then
  print("type() requires an argument")
end

With catch, this becomes natural:

-- Lus: concise error checking
if not catch type() then
  print("type() requires an argument")
end

Testing error conditions#

Test suites frequently need to verify that operations fail as expected. The Lua pattern requires nested function calls:

-- Lua: asserting an error
assert(not pcall(function() error("expected") end))

With catch, assertions about errors read more naturally:

-- Lus: asserting an error
assert(not catch error("expected"))