Lesson 14 - Handling errors in Swift
In the previous lesson, Overloading operators and other useful syntax in Swift, we talked about operator overloading and did a few more tricks in Swift.
In today's Swift tutorial, we're going to explain the last important topic before we move to form applications. The topic is errors. Since we'll stumble across errors in form applications a lot, we're going to learn how to handle them today.
Errors
In programming, errors occur quite frequently as you might have noticed Some are our fault, some are just going to occur no matter what. A typical example is reading data from a file or web service which is not available at the moment. The reasons might be another programmer's mistake, no Internet connection and so on.
In this tutorial, we'll show how to handle those errors at runtime, so the program won't crash.
Swift, just like other modern languages, use a concept of exceptions. An exception refers to the error state of the application. If you come from another language, it might be very familiar to you. However, the "exception" term isn't used here and several things work a little differently.
Running dangerous code
First, let's have a look at how to handle a code that can possibly cause an
error. Swift is more strict in this area and functions that can cause an error
have to be marked with the throws
keyword written before the return
type. A similar rule exists e.g. in Java. Then, we can't work
with the function as we are used to.
Let's have a look at a sample code writing to a file. Create a new Command
Line Tool project and name it ErrorFile
. Put the following code in
the main.swift
file:
// This code doesn't work yet let documentsUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] as NSURL let fileUrl = documentsUrl.appendingPathComponent("info.txt")! "ICT.social is awesome!".write(to: fileUrl, atomically: true, encoding: String.Encoding.unicode)
The code above maybe looks scary, but it's nothing complex. Let's describe it.
- The first line retrieves the path to the Documents folder from the system.
- The second line appends the filename, which we're going to write to, to the path.
- The third line uses a
String
method to write to a file. The rest of the parameters deal with a file lock and encoding.
But this code won't work just yet. It's because we didn't handle the error
that might occur when we fail to write to the file. The error mechanism in Swift
is fairly similar to the Optional
mechanism since we have to deal
with all unexpected states. The write()
method is marked with the
throws
keyword. It means that as soon as we use it we have to
handle all possible error states.
Try
We can solve the problem using the try
keyword which we write at
the beginning of the third line. But not just that alone.
The simplest approach is to write try?
which is again very
similar to the Optional
concept. If something fails, nothing will
simply happen and the program continues. If we used try?
together
with a method which returns a value, we'll get an Optional
which is
empty in the case of an error.
We can also use try!
. Again, the exclamation mark represents a
dangerous situation here, and it practically tells Swift that no error can
happen and that's why we don't want to deal with possible error states. If an
error occurs anyway, the program crashes.
A do-catch block
The most common approach is probably wrapping try
into the
do-catch
block. See the notation below:
do { try "ICT.social is awesome!".write(to: fileUrl, atomically: true, encoding: String.Encoding.unicode) } catch { print("Failed to write to the file") }
The do
block allows us to use the try
keyword
alone. If an error occurs, the catch
block is executed. There we
can access the error
variable and retrieve the particular error if
something went wrong. We can also get some additional information from the
error. In further lessons, we'll show how to react to different error types
using multiple catch
blocks. After executing the code above, you
can see a new text file in your Documents folder to which we wrote some
text.
A defer block
The defer
block executes its code after the block with
try
has finished. That means, in case the try
was successful, but also in case the catch
block was executed
etc.
Before we look at how to use the defer
block, let's put the
previous code into a method. We can leave it in the main.swift
file. Such a method is actually called a function because a method always
belongs to a class.
func writeToFile(text: String) { let documentsUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] as NSURL let fileUrl = documentsUrl.appendingPathComponent("info.txt")! do { try text.write(to: fileUrl, atomically: true, encoding: String.Encoding.unicode) } catch { print("Failed to write to the file, because: \(error)") } }
In order to make the example as simple as possible, let's add a basic logging
after our method is called. We'll add a defer
block to the
beginning:
func writeToFile(text: String) { defer { print("The writeToFile() method was executed") } let documentsUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0] as NSURL let fileUrl = documentsUrl.appendingPathComponent("info.txt")! do { try text.write(to: fileUrl, atomically: true, encoding: String.Encoding.unicode) } catch { print("Failed to write to the file, because: \(error)") } // here the defer block is processed }
Even though the defer
block is declared first, it's executed not
before the program leaves the method's scope (the braces block) in which the
defer
is used. In our case, when the program exits the method. This
block is always executed, even if the method finishes or is
left because it returned something by return
. We could use it in
e.g. a loop as well. Then it's executed when the loop is interrupted by
break
or when it ends naturally. Basically, there's no way the code
isn't executed. That's why it's called defer
, we put there the code
to be executed later. Typically, a clean up is performed there, for example
freeing memory and such actions which have to be done regardless of whether the
dangerous operation went successfully or not. We'll see this mechanism again in
further courses.
Now we know how to use functions that can cause errors.
In our apps, we'll handle errors from library functions more often, however, we'll learn how to raise our own errors too. That's the next lesson's topic, Enums and custom errors in Swift.