What is defer in Swift?
Published on: April 29, 2024Sometimes, 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.