Acquis 20 - Catch Handlers

Contents (17)
  1. Syntax
  2. Handler behavior
    1. On error
    2. On success
  3. Common patterns
    1. Error normalization
    2. Logging errors
    3. Error translation
    4. Chained handlers
  4. Handler errors
  5. Inline handlers
  6. Handler validation
  7. Comparison with xpcall
  8. Motivation
    1. Post-processing errors
    2. Consistent error handling
    3. Separation of concerns

The catch expression supports an optional handler function that transforms errors before they are returned. This allows custom error processing without additional code after the catch.

Syntax#

A handler is specified in brackets between catch and the expression:

local success, ... = catch[handler] <expression>

When the expression raises an error, the handler function is called with the error message. The handler’s return value replaces the original error in the catch result.

-- Basic handler that extracts just the message
local function simplify(err)
    return err:match("^[^:]+:[^:]+: (.+)") or err
end

local ok, err = catch[simplify] error("something went wrong")
assert(ok == false)
assert(err == "something went wrong")  -- simplified error

Handler behavior#

On error#

When the expression raises an error:

  1. The handler is called with the error object
  2. The handler’s return value becomes the second return value
  3. success is false
local function wrap(err)
    return { message = err, timestamp = os.time() }
end

local ok, result = catch[wrap] error("database connection failed")
assert(ok == false)
assert(type(result) == "table")
assert(result.message:match("database connection failed"))

On success#

When the expression evaluates successfully, the handler is not called:

local called = false
local function handler(err)
    called = true
    return err
end

local ok, val = catch[handler] (1 + 2)
assert(ok == true)
assert(val == 3)
assert(called == false)  -- handler was never invoked

Common patterns#

Error normalization#

Standardize error formats across different sources:

local function normalize(err)
    if type(err) == "string" then
        return { code = "UNKNOWN", message = err }
    elseif type(err) == "table" then
        return err
    else
        return { code = "UNKNOWN", message = tostring(err) }
    end
end

local ok, err = catch[normalize] someOperation()
if not ok then
    print("Error " .. err.code .. ": " .. err.message)
end

Logging errors#

Log errors while still returning them:

local function logged(err)
    io.stderr:write("[ERROR] " .. tostring(err) .. "\n")
    return err
end

local ok, result = catch[logged] riskyOperation()
-- Error is logged AND returned for handling

Error translation#

Convert technical errors to user-friendly messages:

local translations = {
    ["ECONNREFUSED"] = "Could not connect to server",
    ["ETIMEDOUT"] = "Connection timed out",
}

local function translate(err)
    for code, message in pairs(translations) do
        if err:find(code) then
            return message
        end
    end
    return "An unexpected error occurred"
end

local ok, err = catch[translate] connect(host, port)
if not ok then
    showUserError(err)  -- user-friendly message
end

Chained handlers#

Handlers can be composed for complex error processing:

local function compose(f, g)
    return function(x) return f(g(x)) end
end

local logAndSimplify = compose(simplify, logged)
local ok, err = catch[logAndSimplify] operation()

Handler errors#

If the handler itself throws an error, that error propagates normally (it is not caught):

local function badHandler(err)
    error("handler failed")  -- this propagates!
end

-- This will raise "handler failed", not return it
catch[badHandler] error("original error")

To catch handler errors, nest another catch:

local ok, result = catch (catch[badHandler] error("original"))

Inline handlers#

Anonymous functions work directly in the handler position:

local ok, err = catch[function(e) return e:upper() end] error("oops", 0)
assert(err == "OOPS")

Handler validation#

The bracketed expression must evaluate to a function. Any other value raises an error at the catch site, so a misspelled handler name is reported instead of being silently ignored:

local ok, err = catch (function()
    catch[42] error("x")
end)()
assert(err:find("attempt to use a number value as a catch handler"))

Only true functions are accepted, matching the xpcall baseline. A table with a __call metamethod is rejected the same way.

Comparison with xpcall#

Lua’s xpcall provides similar functionality but operates at the function level:

-- Lua: xpcall with error handler
local ok, result = xpcall(function()
    return someOperation()
end, function(err)
    return "Handled: " .. err
end)

With catch handlers:

-- Lus: catch with handler
local ok, result = catch[function(e) return "Handled: " .. e end] someOperation()

The catch handler syntax is more concise for single expressions and doesn’t require wrapping the protected code in a function.


Motivation#

Post-processing errors#

Without handlers, error transformation requires additional code after the catch:

-- Without handler
local ok, err = catch operation()
if not ok then
    err = transform(err)
end

With handlers, the transformation is part of the catch:

-- With handler
local ok, err = catch[transform] operation()

Consistent error handling#

Handlers encourage consistent error processing by making the transformation explicit at the catch site rather than scattered throughout the code.

Separation of concerns#

The handler separates “what to do with errors” from “what operation to try”, making code easier to read and maintain.