-
-
Notifications
You must be signed in to change notification settings - Fork 2
Tiri Defer Syntax
The defer statement in Tiri provides Go-style deferred execution, allowing you to schedule cleanup code that runs automatically when a scope exits. This tutorial explains defer patterns with practical examples.
- Basic Defer Syntax
- Understanding Execution Order
- Upvalue Capture vs Argument Snapshot
- Resource Cleanup Patterns
- Defer with Multiple Arguments
- Scope and Control Flow
- Common Patterns and Idioms
- Best Practices
- Limitations
The simplest defer schedules a function to execute when the current scope exits:
function processFile()
file = obj.new('file', { path='data.txt' })
defer
print('Closing file')
file.acFree()
end
-- Work with file
content = file.acRead()
print('Processing: ' .. content)
end
-- Output:
-- Processing: [file content]
-- Closing fileThe deferred function executes after the normal function body completes but before control returns to the caller.
When multiple defer statements exist in the same scope, they execute in LIFO order (Last In, First Out) - the last defer registered executes first:
function demonstrateLifo()
defer
print('First defer registered')
end
defer
print('Second defer registered')
end
defer
print('Third defer registered')
end
print('Function body')
end
-- Output:
-- Function body
-- Third defer registered
-- Second defer registered
-- First defer registeredThis LIFO ordering matches the natural resource acquisition pattern - resources acquired last should be released first:
function acquireResources()
db = connectDatabase()
defer
db:disconnect()
end
file = openFile('log.txt')
defer
file:close()
end
lock = acquireLock()
defer
lock:release()
end
-- Work with resources
end
-- Cleanup order: lock released, file closed, database disconnectedUnderstanding the difference between upvalue capture and argument snapshot is crucial for correct defer usage.
When defer has no arguments, it captures variables as upvalues - the deferred function sees the current value at execution time:
function demonstrateUpvalues()
status = 'initial'
defer
print('Status: ' .. status) -- Captures 'status' as upvalue
end
status = 'modified'
status = 'final'
end
-- Output: Status: finalThe defer sees status at the time it executes, which is 'final'.
When defer receives arguments via end(...), those values are snapshotted at registration time:
function demonstrateSnapshot()
status = 'initial'
defer(s)
print('Status: ' .. s) -- 's' is snapshotted
end(status)
status = 'modified'
status = 'final'
end
-- Output: Status: initialThe argument s receives the value of status at the time defer was registered, which is 'initial'.
Use upvalue capture when:
- You want the cleanup to see the latest state
- The variable won't change unexpectedly
- You're calling methods on objects (the object reference doesn't change)
function simpleFileCleanup()
file = obj.new('file', { path='data.txt' })
defer
file.acFree() -- Object reference won't change
end
-- ... use file ...
endUse argument snapshot when:
- You need to capture original values for logging, metrics, or rollback
- Variables might be reassigned during function execution
- You're passing primitive values that could change
function trackOperationDuration()
startTime = os.time()
operationId = generateId()
defer(opId, start)
duration = os.time() - start
print('Operation ' .. opId .. ' took ' .. duration .. ' seconds')
end(operationId, startTime)
-- operationId and startTime might be reused for nested operations
-- but defer will log the original values
endfunction processTextFile(filename)
file = obj.new('file', { path=filename })
defer
print('Closing: ' .. filename)
file.acFree()
end
content = file.acRead()
-- Process content
return content
endWhen resources have dependencies, use multiple defers. They execute in reverse order, ensuring proper cleanup:
function transferData(sourcePath, destPath)
-- Open source file
source = obj.new('file', { path=sourcePath, flags='READ' })
defer(path)
print('Closing source: ' .. path)
source.acFree()
end(sourcePath)
-- Open destination file
dest = obj.new('file', { path=destPath, flags='WRITE|NEW' })
defer(path)
print('Closing destination: ' .. path)
dest.acFree()
end(destPath)
-- Transfer data
content = source.acRead()
dest.acWrite(content)
end
-- Output:
-- Closing destination: [destPath]
-- Closing source: [sourcePath]You can use upvalues to conditionally execute cleanup:
function conditionalCleanup(filename, deleteOnError)
file = obj.new('file', { path=filename, flags='WRITE|NEW' })
shouldDelete = deleteOnError
defer
file.acFree()
if shouldDelete then
obj.delete(filename)
print('Deleted temporary file: ' .. filename)
end
end
-- If an error occurs, shouldDelete remains true
-- On success, set it to false
file.acWrite('data')
shouldDelete = false
endDefer can capture multiple arguments for complex cleanup scenarios:
function advancedCleanup()
resource = acquireResource('alpha')
token = 'session-123'
timestamp = os.time()
defer(res, tkn, ts)
print('Cleanup started at: ' .. ts)
print('Token: ' .. tkn)
res:release()
end(resource, token, timestamp)
-- Even if these change, defer uses original values
resource = acquireResource('beta')
token = 'session-456'
timestamp = os.time()
endYou can pass cleanup functions as arguments for flexible resource management:
function withCustomCleanup(resource, cleanupHandler)
defer(handler, res)
print('Running custom cleanup')
handler(res)
end(cleanupHandler, resource)
-- Work with resource
end
-- Usage
withCustomCleanup(myResource, function(r)
r:close()
print('Custom cleanup complete')
end)Defers execute when leaving a scope via any path: normal completion, return, break, or continue.
function validateAndProcess(data)
file = obj.new('file', { path='output.txt' })
defer
print('Cleanup in defer')
file.acFree()
end
if not data then
print('Invalid data')
return nil -- Defer still executes before return
end
file.acWrite(data)
return true
end
-- Output (on invalid data):
-- Invalid data
-- Cleanup in deferDefers in loop scope execute when breaking:
function processUntilError(items)
for i, item in ipairs(items) do
temp = createTemporary(item)
defer
print('Cleaning iteration ' .. i)
temp:delete()
end
if not processItem(item) then
print('Error at item ' .. i)
break -- Defer executes before breaking
end
end
endDefers also execute on continue:
function processFiltered(items)
for i, item in ipairs(items) do
resource = acquire(item)
defer
print('Releasing resource ' .. i)
resource:release()
end
if not shouldProcess(item) then
continue -- Defer executes before continuing
end
process(item)
end
endDefers respect scope boundaries:
function demonstrateScopes()
defer
print('Outer defer')
end
do
defer
print('Inner defer')
end
print('Inner scope body')
end -- Inner defer executes here
print('Outer scope body')
end -- Outer defer executes here
-- Output:
-- Inner scope body
-- Inner defer
-- Outer scope body
-- Outer deferfunction performTransaction(operations)
txn = db:beginTransaction()
committed = false
defer
if not committed then
print('Rolling back transaction')
txn:rollback()
end
end
for _, op in ipairs(operations) do
txn:execute(op)
end
txn:commit()
committed = true
endfunction measureExecution(taskName)
startTime = os.time()
defer(name, start)
duration = os.time() - start
print(name .. ' took ' .. duration .. ' seconds')
end(taskName, startTime)
-- Perform task
performLongOperation()
endfunction withLock(lockName, fn)
lock = acquireLock(lockName)
defer
print('Releasing lock: ' .. lockName)
lock:release()
end
return fn()
end
-- Usage
withLock('resource-lock', function()
-- Critical section
modifySharedResource()
end)function withTempFile(fn)
tempPath = '/tmp/work-' .. os.time()
file = obj.new('file', { path=tempPath, flags='WRITE|NEW' })
defer(path)
file.acFree()
obj.delete(path)
print('Deleted temp file: ' .. path)
end(tempPath)
return fn(file)
endfunction withModifiedState(object, newState)
previousState = object.state
object.state = newState
defer(obj, oldState)
print('Restoring state to: ' .. oldState)
obj.state = oldState
end(object, previousState)
-- Work with modified state
processObject(object)
end-- Good: defer right after acquisition
function good()
file = obj.new('file', { path='data.txt' })
defer
file.acFree()
end
-- ... use file ...
end
-- Bad: defer far from acquisition
function bad()
file = obj.new('file', { path='data.txt' })
-- Many lines of code
doSomething()
doSomethingElse()
defer
file.acFree() -- Easy to forget or misplace
end
end-- Good: snapshot values that might change
function good()
id = generateId()
defer(capturedId)
logCompletion(capturedId)
end(id)
id = generateNewId() -- Original id is preserved in defer
end
-- Bad: upvalue might have wrong value
function bad()
id = generateId()
defer
logCompletion(id) -- Will use modified id
end
id = generateNewId()
end-- Good: simple, focused cleanup
defer
resource:close()
end
-- Bad: complex logic in defer
defer
if resource.type is 'file' then
resource:close()
elseif resource.type is 'socket' then
resource:disconnect()
else
-- Complex cleanup logic
end
endfunction processWithLogging(data)
startTime = os.time()
-- Log completion time when function exits
defer(start)
elapsed = os.time() - start
print('Processing completed in ' .. elapsed .. 's')
end(startTime)
-- Process data
return transform(data)
endDefers do not currently execute during error unwinding (stack unwinding from exceptions):
function limitationExample()
defer
print('This will NOT execute if error() is called')
end
error('Something went wrong')
endFor error-safe cleanup, use pcall:
function errorSafeCleanup()
resource = acquire()
ok, result = pcall(function()
defer
resource:release()
end
-- Work that might error
riskyOperation()
end)
if not ok then
-- Handle error
print('Error: ' .. result)
end
endErrors in deferred functions propagate normally:
function deferError()
defer
error('Defer failed')
end
print('Body')
end
-- Output:
-- Body
-- [Error: Defer failed]To prevent defer errors from aborting execution, use pcall inside the defer:
function safeDeferError()
defer
pcall(function()
riskyCleanup()
end)
end
print('Body')
endDefer must be used within a function or method:
-- Invalid: defer at module level
defer
print('cleanup')
end
-- Valid: defer inside function
function valid()
defer
print('cleanup')
end
endThe defer statement provides:
- Automatic cleanup: Resources are released when scope exits
- LIFO ordering: Last defer registered executes first
- Guaranteed execution: Runs on normal exit, return, break, and continue
- Flexible capture: Choose between upvalue capture or argument snapshot
Use defer to write cleaner, safer code with deterministic resource management.
See Also:
- Tiri Reference Manual - Complete Tiri language reference