Acquis 20 - Catch Handlers
Contents (17)
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:
- The handler is called with the error object
- The handler’s return value becomes the second return value
successisfalse
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.