What is defer in Swift?

Published on: April 29, 2024

Sometimes, we write code that needs set some state or perform some work at the start of a function and at the end of that same function we might have to reset that state, or perform some cleanup regardless of why we’re exiting that function.

For example, you might have a function that creates a new Core Data object and depending on whether you’re able to enrich the object with data from the network you want to exit the function early. Regardless of how and why you exit the function, you want to save your newly created object.

Writing our code without defer

Here’s what that code would look like without Swift’s defer statement

func createMovie(
  named title: String,
  in context: NSManagedObjectContext
) async throws -> Movie {

  let movie = Movie(context: context)
  movie.title = title

  guard let data = try? await network.fetchExtraMovieData() else {
    try context.save()
    return movie
  }

  movie.rating = data.rating

  try context.save()
  return movie
}

Let me start by saying that there are other ways to write this code; I know. The point isn’t that we could refactor this code to have a single return statement. The point is that we have multiple exit points for our function, and we have to remember to call try context.save() on every path.

Cleaning up our code with defer

With Swift’s defer we can clean our code up by a lot. The code that we write in our defer block will be run whenever we’re about to leave our function. This means that we can put our try context.save() code in the defer block to make sure that we always save before we return, no matter why we return:

func createMovie(
  named title: String,
  in context: NSManagedObjectContext
) async -> Movie {

  let movie = Movie(context: context)
  movie.title = title

  defer {
    do {
      try context.save()
    } catch {
      context.rollback()
    }
  }

  guard let data = try? await network.fetchExtraMovieData() else {
    return movie
  }

  movie.rating = data.rating

  return movie
}

Notice that we changed more that just dropping a defer in our code. We had to handle errors too. That’s because a defer block isn’t allowed to throw errors. After all, we could be leaving a function because an error was throw; in that case we can’t throw another error.

Where can we use a defer block?

Defer blocks can be used in functions, if statements, closures, for loops, and any other place where you have a “scope” of execution. Usually you can recognize these scopes by their { and } characters.

If you add a defer to an if statement, your defer will run before leaving the if block.

Defer and async / await

Defer blocks in Swift run synchronously. This means that even when you defer in an async function, you won’t be able to await anything in that defer. In other words, a defer can’t be used as an asynchronous scope. If you find yourself in need of running async work inside of a defer you’ll have to launch an unstructured Task for that work.

While that would allow you to run async work in your defer, I wouldn’t recommend doing that. Your defer will complete before your task completes (because the defer won’t wait for your Task to end) which could be unexpected.

In Summary

Swift’s defer blocks are incredibly useful to wrap up work that needs to be done when you exit a function no matter why you might exit the function. Especially when there are multiple exit paths for your function.

Defer is also useful when you want to make sure that you keep your “start” and “finish” code for some work in a function close together. For example, if you want to log that a function has started and ended you could write this code on two consecutive lines with the “end” work wrapped in defer.

In my experience this is not a language feature that you’ll use a lot. That said, it’s a very useful feature that’s worth knowing about.

Subscribe to my newsletter