As we’ve seen in my article on Swift’s guard Statement and this article on error handling, Swift’s guard
and throw
statements encourage a style of programming where we return early from a given scope in case of error.
In the case of the guard
statement, this is due to a failing precondition and in the case of the throw
statement, this is because we encountered some sort of error. Although this exit-early approach does help clean up our code, returning early from a given scope also poses it’s own problems, primarily how to ensure that any resources that were created within the scope are reliably cleaned up before exiting. In this post, we look at how to solve these types of a problem using Swift’s defer
Statement.
Table of Contents
The Problem
Before we dig into the details of the defer
statement itself, let’s first kick off by taking a closer look at the problem the defer
statement is trying to solve. The best way of doing this is via an example.
Just imagine we had some sort of function that writes a string (in this case Hello World
) to a pre-existing file (trial.txt
) on disk. In Swift, the function may look something like this:
func writeDataToFile() throws {
// 1
let filename = "trial.txt"
let documentPath = NSSearchPathForDirectoriesInDomains(
.documentDirectory, .userDomainMask, true)[0]
let path = documentPath + "/" + filename
// 2
print("Opening file...")
guard let file = FileHandle(forUpdatingAtPath: path) else {
print("Failed to open file")
throw FileError.notFound
}
// 3
print("Fetching data...")
let data = ("Hello World" as NSString).data(
using: String.Encoding.utf8.rawValue)
// 4
file.seek(toFileOffset:0)
// 5
print("Writing data to file...")
file.write(data!)
// 6
print("Closing file...")
file.closeFile()
}
do {
try writeDataToFile()
} catch {
print("Encountered error \(error)")
}
// Opening file...
// Fetching data...
// Writing data to file...
// Closing file...
It’s actually relatively straightforward.
First, we construct a path to the file (1
). We then open the file for updating and obtain a handle to the opened file (2
). We then construct our data to write to the file (3
), seek to the start of the file (4
), write our data (5
), and then close the file (6
). Relatively simple.
The only real complication we might encounter is that we may fail to open the file (for example if it didn’t exist) in which case we throw an error and return.
However, now imagine that instead of writing a simple string like Hello World
to the file the function instead called another function (fetchData()
) to get hold of the data it was going to write. Also imagine that that call required some intensive computation and could potentially result in an error being thrown (3
):
func writeDataToFile() throws {
// 1
let filename = "trial.txt"
let documentPath = NSSearchPathForDirectoriesInDomains(
.documentDirectory, .userDomainMask, true)[0]
let path = documentPath + "/" + filename
// 2
print("Opening file...")
guard let file = FileHandle(forUpdatingAtPath: path) else {
print("File open failed")
throw FileError.notFound
}
// 3
print("Fetching data...")
let data = try fetchData()
// 4
file.seek(toFileOffset:0)
// 5
print("Writing data to file...")
file.write(data)
// 6
print("Closing file...")
file.closeFile()
}
do {
try writeDataToFile()
} catch {
print("Encountered error \(error)")
}
// Opening file...
// Fetching data...
// Encountered error fetchError
The fact that this error can be thrown leaves us with an issue.
In this example, our call to the throwing function fetchData()
does actually result in an error being thrown.
As we learnt in my post on error handling when an error is thrown it can either be caught and handled (using something like a do-catch
statement) or propagated back to the calling code as we have done in this example (which if you recall requires the function to be marked with the keyword throws
).
In the case of propagation, any encountered error causes execution of the current scope to immediately exit and the error propagation to occur. The result is that steps 4
to 6
of the writeDataToFile()
function are skipped and therefore, fail to close our file properly.
This is where the defer
statement comes in.
The defer
Statement
The defer
statement provides a clean way to handle these types of a situation by declaring a block of statements that will be executed when execution leaves the current scope. It’s almost like pushing the statements within the defer
statement onto a stack for later execution. We’ll revisit this analogy later.
On top of this deferred execution, the added bonus with the defer
statement is that its block of statements are actually guaranteed to be executed regardless of how execution leaves the current scope. This includes situations where we exit due to an error being thrown or because of statements such as a return
or a break
statement being executed. As you can imagine, this makes the defer
statement ideal for cleaning up previously created resources such as file descriptors (as in this example), manually allocated memory or closing connections to a database.
So this is all well and good, but what does the defer
statement look like in practice? The good news here is that the defer
statement is actually super simple and has the following general form:
defer {
// Statements
}
As you can see, it’s just the defer
keyword followed by a set of curly braces that enclose a set of Swift statements. These are the statements that will be executed when the current scope exits. With this in mind let’s tweak our example:
func writeDataToFile() throws {
// 1
let filename = "trial.txt"
let documentPath = NSSearchPathForDirectoriesInDomains(
.documentDirectory, .userDomainMask, true)[0]
let path = documentPath + "/" + filename
// 2
print("Opening file...")
guard let file = FileHandle(forUpdatingAtPath: path) else {
print("File open failed")
throw FileError.notFound
}
// 6
defer {
print("Closing file...")
file.closeFile()
}
// 3
print("Fetching data...")
let data = try fetchData()
// 4
file.seek(toFileOffset:0)
// 5
print("Writing data to file...")
file.write(data)
}
do {
try writeDataToFile()
} catch {
print("Encountered error \(error)")
}
// Opening file...
// Fetching data...
// Closing File...
// Encountered error fetchError
Execution starts much like our previous examples with the construction of the path to the file (1
), and opening the file for updating (2
). However, after that point, things are a little different as we next encounter our new defer
statement (6
).
As mentioned earlier, the statements enclosed within the defer
statement are only executed when execution leaves the current scope not when the defer
statement is encountered so at this point, Swift simply makes a note that it has seen the defer
statement (and it’s enclosed statements) and puts them to one side for later execution. It then continues on it’s way with its call to the fetchData()
function (3
).
As in our previous example, this call to the fetchData()
function results in an error being thrown and like in the last example our error is propagated to the enclosing scope. This time though, instead of immediately returning control to the calling code, Swift first executes the statements within the defer
statement and does so before it returns control. This ensures that our file is closed cleanly before control is returned to the calling code. Great! – exactly what we want.
defer
Statement Limitations
Now, you may at this point be wanting to rush out to use the defer
statement everywhere you can, but before you do there are a few limitations that you should know about. The first is to do with the types of statements we can include within the defer
statement.
For the most part, any Swift statement can be included in the body of a defer
statement but there is one exception to this rule. In order to fulfil the guarantee that all statements within the defer
statement are executed, the defer
statement can’t contain any code that would transfer control out of the defer
statement. This means that we can’t include statements such as the break
or return
statements or throw any errors within the body of the defer
statement as any of these statements would cause execution of that defer
block to be immediately terminated, potentially resulting in subsequent statements not being executed.
That’s not all though. Another thing you should watch out for is using multiple defer
statements within a single scope.
Using Multiple defer
Statements
Using multiple defer
statements within a single scope is not a limitation per se, just something you should watch out for as the resulting execution may not be quite what you expect.
When multiple defer
statements appear in the same scope, each defer
statement is actually executed in a first-in-last-out (FILO) manner – i.e. the set of statements associated with the first defer
block that is encountered and pushed onto the stack are the last set of statements that are actually executed.
One way of thinking about it is to extend our analogy from earlier and think of each defer
statement being a stack.
When the first defer
statement is encountered it is pushed onto the stack. When the second defer
statement is encountered it is also pushed onto the stack but on top of the first etc.
When execution then exits the current scope, each defer
statement on the stack is then popped off the stack from the top of the stack. This results in the defer
statements being executed in the reverse order from which they were encountered.
We can see this from a simplified example below:
func abc() {
defer { print("First defer...") }
defer { print("Second defer...") }
defer { print("Third defer...") }
print("Doing some work...")
}
abc()
// Doing some work...
// Third defer...
// Second defer...
// First defer...
By executing the defer
blocks in this way Swift ensures that everything that was created prior to encountering the defer
block such as constants or variables are still be in scope when the statements within the defer
statement are executed.
A Word of Caution
Ok, one last thing about the defer
statement for today and that is a gentle word of caution.
As we’ve seen, the defer
statement is great mechanism for cleaning up resources, especially given the ‘exit-early’ approach that Swift promotes. However, due to the way it works and this idea of deferred execution if not used carefully the statement can lead to confusing and untraceable code. For example if there is a significant amount of code between the defer
statement being encountered and it actually being executed you have to make a mental note that the code will be executed when the current scope exits. Not a huge problem but still one that makes your code a little more difficult to read. However, there is one thing that can have a much greater impact.
As we’ve seen, the defer
statement is great for deferring the execution of a given set of statements and because of this, you may be tempted to use it for uses other than simply cleaning up created resources. For example, you might be tempted to use the defer
statement to modify values that are returned from a function:
func addOneOrTwo(x: inout Int) {
defer { x = x + 1 }
x = x + 1
}
var x = 1
addOneOrTwo(x: &x)
print("x: \(x)")
// x: 3
Now, although this is clever – this is a case where clever is not actually good. Even this simple example results in an amount of thought for anyone reading your code to work out exactly what is going on. If you scale this up to larger code bases where the defer
statement is hundreds of lines away from the functions return value and the cognitive load increases significantly.
When it boils down to it, although there is nothing to stop you using the defer
statement in this manner it does tend to contravene Swift’s core ethos of clarity so my recommendation is to stick to using it for just resource clean up rather than anything more advanced.