Articles and podcasts about Swift development, by John Sundell.

Genius Scan SDK

Presented by the Genius Scan SDK

Using Swift’s defer keyword within async and throwing contexts

Published on 15 Apr 2025

Swift’s defer keyword allows us to delay the execution of a given block of code until the current scope is exited. While that might initially not seem that useful (after all, can’t we simply write that code at the end of the scope instead?), it turns out that when writing modern Swift code, we’re quite often dealing with multiple potential exit points within our functions and closures — especially when writing code that throws, or when utilizing async/await.

Let’s take a look at the following SearchService type’s loadItems method as an example. It uses a Database API that requires a connection to be opened before any operations can be performed, and that connection then needs to be properly closed and cleaned up before new database requests can be accepted:

actor SearchService {
    private let database: Database
    ...

    func loadItems(maching searchString: String) throws -> [Item] {
        let connection = database.connect()

        do {
            let items: [Item] = try connection.runQuery(.entries(
                matching: searchString
            ))

            connection.close()
            return items
        } catch {
            connection.close()
            throw error
        }
    }
}

Note how we need to explicitly specify the type for our items above, since the runQuery method is generic, as it can return an array of any kind of database-compatible entry type that we’re looking to retrieve.

Because our code has two separate branches (one for when our runQuery call succeeds, and one for when an error is thrown), we need to write separate calls to connection.close within each branch. That might initially not seem like a big deal, but just like most code duplication, it increases the chance that we’ll end up making a mistake, which could result in a quite major bug in this instance (as missing a close call would leave the database unable to accept additional requests).

One way to solve the above problem would be to ensure that our code only has a single branch of execution. In the case of the above loadItems method, that could be done by using the closure-based Result initializer included in the standard library, which converts a throwing closure into a result, which can then be unwrapped once we’ve closed our database connection — like this:

actor SearchService {
    private let database: Database
    ...

    func loadItems(maching searchString: String) throws -> [Item] {
        let connection = database.connect()

        let result = Result<[Item], Error> {
            try connection.runQuery(.entries(matching: searchString))
        }

        connection.close()
        return try result.get()
    }
}

While that’s certainly an improvement — let’s now take a look at how defer lets us solve the problem in an arguably much more elegant way, since it’ll let us define the closing of our database connection right next to where the connection is opened:

actor SearchService {
    private let database: Database
    ...

    func loadItems(maching searchString: String) throws -> [Item] {
        let connection = database.connect()
        defer { connection.close() }

        return try connection.runQuery(.entries(matching: searchString))
    }
}

Nice! Not only are the calls to connect and close now right next to each other (which arguably makes it easier to reason about those two calls as a pair), but because we can now directly return the result of our runQuery call, we no longer have to manually specify any type information — the compiler can now automatically infer the return type of that call for us.

Using defer does have somewhat of a downside, though, in that it sort of breaks the traditional control flow model that structured programming tends to follow — where instructions are always executed from top to bottom. Within our current loadItems implementation, for example, we now have three expressions:

But those expressions won’t be executed in the order 1, 2, 3, but rather in the order 1, 3, 2, which might initially seem like a quite strange way of structuring our code. So, using defer might end up being somewhat of an acquired taste, and a tool that should probably not be over-used, but rather just used when there’s some specific cleanup work that we want to ensure gets performed no matter how the current scope is exited.

Async contexts

The defer keyword is perhaps even more useful in the concurrent world of async/await, since one of the benefits of that way of writing async code is that it lets us “flatten” code that previously required nesting in the shape of closures or separate operations.

For example, within the following ItemListService, we once again have to work with separate code branches (and thus, nesting) in order to ensure that an isLoading bool is set back to false whenever a loading operation was completed:

actor ItemListService {
    private let networking: NetworkingService
    private var isLoading = false
    ...

    func loadItems(after lastItem: Item) async throws -> [Item] {
        guard !isLoading else { throw Error.alreadyLoading }
isLoading = true

        do {
            let request = requestForLoadingItems(after: lastItem)
            let response = try await networking.performRequest(request)
            let items: [Item] = try response.decoded()
            
            isLoading = false
            return items
        } catch {
            isLoading = false
            throw error
        }
    }
}

In this case, we can’t rely on the Result-based approach we took earlier to flatten our code into a single branch, since there’s no built-in way to convert an async closure into a Result (although that’s something we could add, using a custom extension). So this is a type of situation where defer really comes in handy, as it lets us ensure that our isLoading state is always assigned back to false whenever an operation either succeeded or failed:

actor ItemListService {
    private let networking: NetworkingService
    private var isLoading = false
    ...

    func loadItems(after lastItem: Item) async throws -> [Item] {
        guard !isLoading else { throw LoadingError.alreadyLoading }
        isLoading = true
        defer { isLoading = false }

        let request = requestForLoadingItems(after: lastItem)
        let response = try await networking.performRequest(request)
        return try response.decoded()
    }
}

The above type of approach can also be really useful when working with nested async tasks as well. For example, let’s say that we wanted to improve the above loadItems method so that it doesn’t throw an error if called while a loading operation is already underway. To do that, we could keep track of a dictionary of loading tasks (keyed by the ID of the lastItem for each task), and then use defer to ensure that a task is always removed from that dictionary when it was completed — like this:

actor ItemListService {
    private let networking: NetworkingService
    private var activeTasksForLastItemID = [Item.ID: Task<[Item], Error>]()
    ...

    func loadItems(after lastItem: Item) async throws -> [Item] {
        if let existingTask = activeTasksForLastItemID[lastItem.id] {
            return try await existingTask.value
        }

        let task = Task {
            defer { activeTasksForLastItemID[lastItem.id] = nil }

            let request = requestForLoadingItems(after: lastItem)
            let response = try await networking.performRequest(request)
            return try response.decoded() as [Item]
        }

        activeTasksForLastItemID[lastItem.id] = task
return try await task.value
    }
}

In general, the above technique is a neat way of preventing duplicate async actor requests, since actors only protect against simultaneous calls while they’re busy performing synchronous work. Once an actor has started an async task using await, it’s free to accept new calls while that async task is being performed. By using a nested Task combined with the defer keyword, we can ensure that such duplicate requests are properly reused and discarded once finished, all in a predictable manner.

Genius Scan SDK

Swift by Sundell is brought to you by the Genius Scan SDK — Add a powerful document scanner to any mobile app, and turn scans into high-quality PDFs with one line of code. Try it today.

Conclusion

Swift’s defer keyword might initially seem like a somewhat odd language tool, as it doesn’t strictly follow the top-to-bottom control flow order that structured programming tend to use. But when it comes to cleanup operations, state management, and other tasks that we want to ensure are run no matter how a given scope is exited, it can be a really great tool — especially when utilizing Swift concurrency and the language’s native error handling model.

If you’ve got questions, comments, or feedback, then feel free to reach out via either Mastodon or Bluesky.

Thanks for reading!