Defer

The defer statement schedules a statement or block of code to execute when the current scope exits. This is invaluable for ensuring cleanup happens reliably, regardless of how a function returns.

Basic Usage

Place defer before a statement you want to run at scope exit. The deferred statement runs even if the function returns early or an error occurs. This makes defer perfect for resource cleanup.

Most expressions and statements are valid within a defer statement, including function calls, variable assignments, and control flow statements if they do not contain return or try.

func processFile(path string) {
    var file = openFile(path)
    defer file.close()

    // Work with the file
    var data = file.read()
    process(data)

    // file.close() is called automatically when the function returns
}

Defer statements can be used in any scope, not just functions. This includes conditional blocks and loop bodies when extracted to functions.

func example() {
    if condition {
        var temp = allocate()
        defer temp.free()

        useTemp(temp)
    }  // temp.free() runs here

    // temp is not accessible here
}

Multiple Defer Statements

You can have multiple defer statements in the same scope. They execute in reverse order, like a stack.

This last-in-first-out ordering ensures that resources are released in the opposite order they were acquired, which is usually what you want.

func complexOperation() {
    defer c.printf("Third\n")
    defer c.printf("Second\n")
    defer c.printf("First\n")

    c.printf("Running\n")
}

// Output:
// Running
// First
// Second
// Third

Defer with Blocks Proposed

For more complex cleanup, use a block with defer.

func initializeSubsystems() {
    var graphics = initGraphics()
    defer {
        c.printf("Shutting down graphics\n")
        graphics.cleanup()
    }

    var audio = initAudio()
    defer {
        c.printf("Shutting down audio\n")
        audio.cleanup()
    }

    // Both subsystems will be cleaned up in reverse order
}

Common Patterns

The most common use of defer is for resource cleanup. Files, network connections, locks, and memory allocations all benefit from deferred cleanup.

func readConfig(path string) Config {
    var file = openFile(path)
    defer file.close()

    return parseConfig(file.read())
}

Another common pattern is pairing acquire and release operations.

func criticalSection() {
    mutex.lock()
    defer mutex.unlock()

    // Critical section code
    // Mutex is always unlocked, even if we return early
}

Defer in Loops

Unlike some languages, Conjure’s defer implementation works more like a copy/paste of the deferred instructions. This means that defer statements inside loops will execute at the end of each iteration, not just once when the function exits.

Conjure is also smart enough to avoid accumulating deferred statements across iterations and scopes. Each iteration’s defer statements are independent and only certain control flow statements cause the parent scope’s defer statements to be injected.

func processFiles(paths []string) {
	var outerFile = try openFile(paths[0])
	defer outerFile.close()

    for path in paths {
        var innerFile = try openFile(path)
        defer innerFile.close()

		if someCondition {
			// innerFile.close() will run here
			continue
		}

        process(innerFile)

		// innerFile.close() will run here
    }

	// outerFile.close() will run here
}

However, this has a similar code size impact as if you manually placed the cleanup code at the end of each loop iteration.

func processFiles(paths []string) {
	var outerFile = try openFile(paths[0])
	defer outerFile.close()

    for path in paths {
        var innerFile = try openFile(path)
        defer innerFile.close()

		if someCondition {
			// innerFile.close() will run here
			continue
		}

		if someOtherCondition {
			// innerFile.close() will run here
			// outerFile.close() will run here
			return
		}

		process(innerFile)

		// innerFile.close() will run here
    }

	// outerFile.close() will run here
}

Defer Best Practices

Schedule cleanup with defer immediately after acquiring a resource. This ensures you don’t forget to clean up and makes the pairing visually obvious.

// Good
var file = openFile(path)
defer file.close()

// Bad - easy to forget cleanup
var file = openFile(path)
// ... many lines of code ...
defer file.close()

Use defer for any operation that must happen regardless of how a function exits. This includes releasing locks, closing files, freeing memory, and restoring state.