Skip to content

Tiri Defer Syntax

Paul Manias edited this page Feb 11, 2026 · 2 revisions

Tiri Defer Syntax Patterns

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.

Table of Contents

Basic Defer Syntax

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 file

The deferred function executes after the normal function body completes but before control returns to the caller.

Understanding Execution Order

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 registered

This 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 disconnected

Upvalue Capture vs Argument Snapshot

Understanding the difference between upvalue capture and argument snapshot is crucial for correct defer usage.

Upvalue Capture (No Arguments)

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: final

The defer sees status at the time it executes, which is 'final'.

Argument Snapshot (With Arguments)

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: initial

The argument s receives the value of status at the time defer was registered, which is 'initial'.

When to Use Each

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 ...
end

Use 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
end

Resource Cleanup Patterns

File Handling

function processTextFile(filename)
   file = obj.new('file', { path=filename })

   defer
      print('Closing: ' .. filename)
      file.acFree()
   end

   content = file.acRead()
   -- Process content
   return content
end

Multiple Resources with Dependencies

When 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]

Conditional Cleanup

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
end

Defer with Multiple Arguments

Defer 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()
end

Passing Functions as Arguments

You 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)

Scope and Control Flow

Defers execute when leaving a scope via any path: normal completion, return, break, or continue.

Early Return

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 defer

Break in Loops

Defers 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
end

Continue in Loops

Defers 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
end

Nested Scopes

Defers 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 defer

Common Patterns and Idioms

Transaction Rollback Pattern

function 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
end

Timing Measurements

function measureExecution(taskName)
   startTime = os.time()

   defer(name, start)
      duration = os.time() - start
      print(name .. ' took ' .. duration .. ' seconds')
   end(taskName, startTime)

   -- Perform task
   performLongOperation()
end

Lock Acquisition Pattern

function 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)

Temporary File Pattern

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)
end

State Restoration Pattern

function 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

Best Practices

1. Place Defers Immediately After Resource Acquisition

-- 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

2. Use Argument Snapshot for Values That Might Change

-- 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

3. Keep Deferred Functions Simple

-- 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
end

4. Document Defer Intent

function 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)
end

Limitations

Error Path Handling

Defers 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')
end

For 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
end

Defer Errors Propagate

Errors 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')
end

No Defer Outside Function Scope

Defer 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
end

Summary

The 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:

Clone this wiki locally