Acquis 20 - Catch Handlers
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")
assert(err == "OOPS")
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.