defer
defer is a way to define code statements to execute at the end of scope.
Typically, this would be for cleanup tasks.
It’s ideal for situations where there’s more than one way to exit the current scope - more than one return, a throw, etc.
I feel like I don’t often see it , and sometimes forget to use it myself - despite defer existing since the early days.
Example - there’s a bug here
func loadContent() async {
//Show loading indicator and clear old data
isFetching = true
items = []
do {
//Update items and turn off loading data
items = try await fetchData()
isFetching = false
}
catch {
//TODO: handle error
print("Loading error: \(error)")
}
}
Note, we don’t reset isFetching in the catch.
We could add the missing reset to the catch
func loadContent() async {
//Show loading indicator and clear old data
isFetching = true
items = []
do {
//Update items and turn off loading data
items = try await fetchData()
isFetching = false
}
catch {
//TODO: handle error
print("Loading error: \(error)")
isFetching = false
}
}
But if our function has additional exit points, the issue returns
do {
//Update items and turn off loading data
let fetchedItems = try await fetchData()
guard !Task.isCancelled else {
//forgot it here!
return
}
if fetchedItems.isEmpty {
//and here
throw PlaygroundError.emptyItems
}
else {
items = fetchedItems
isFetching = false
}
}
catch {
...
The defer block
defer {
isFetching = false
}
Deter blocks can be anywhere in the scope, but typically close to related statements.
You can have multiple defer statements if you need to clean up or reset multiple things.
It’s async compatible - doing an await within a scope doesn’t cause that scope to end.
defer blocks allow for a neater solution
func loadContent() async {
//Show loading indicator and clear old data
isFetching = true
items = []
defer {
isFetching = false
}
do {
//Update items
let fetchedItems = try await fetchData()
items = fetchedItems
// no need to reset isFetching here
}
catch {
print("Loading error: \(error)")
// no need to reset it here
}
}
Other solutions to this specific problem
This example is overly simplistic, and could be solved by restructuring. This particular example of a loading state only while fetching could also be neatly solved with Alex’s Loadable enum, which makes storing the data or the loading state mutually exclusive.
defer could also be used to close anything session or context based, delete temporary files, any item its important to set something to false or call a close, save, finish type method when done with something.
Where doesn’t defer help?
Any code structure where scope ends “early” in your flow, or has multiple scopes, for example delegate callbacks and closures.
This might explain defer not being that common, but with async await having existed for many years now, those patterns are less and less common.
Here is an example of defer that wouldn’t work with a closure based fetch:
func loadContent() async {
//Show loading indicator and clear old data
isFetching = true
items = []
defer {
isFetching = false
}
fetchDataOutdated(completion: { result in
switch result {
case .success(let data):
self.items = data
case .failure(let error):
print("fetch error: \(error)")
}
})
//execution hits here, ending scope before escaping closure is called
}