Regardless of your skill, handling errors is a common task for any developer. As developers we have complete control of the code we write and features we include, but despite our best efforts, we don’t control everything. We don’t control the values our users enter or whether our applications have access to the resources they need. In this post we’re going to look at how we can handle these situations by looking at error handling in Swift.
Table of Contents
Error Conditions
In Swift terminology an error condition is:
…a failed condition check that results in a portion of our code skipping its usual course of execution.”
As such error conditions generally fall into one of three camps:
- Logical Error Conditions
- Simple Error Conditions
- Recoverable Error Conditions
Let’s look at each of these in turn.
Logical Error Conditions
Logical error conditions are error conditions that are introduced as a result of the code we write. This category of error condition include things like accidentally calling a method on a variable we think is holding an object when the variable is in fact nil
or trying to access an element on a collection using an out-of-range subscript.
For the most part, the Swift language (with the help of the compiler), does its best to help us avoid these kinds of error condition but it is not perfect and when these types of error conditions do occur it usually leads to our apps crashing.
Simple Error Conditions
The next category of error conditions are the simple error conditions. Simple error conditions are errors that occur as a result of performing some kind of operation that can fail in some obvious way. The error conditions in this category are usually simple in nature and because of this they are usually easily understandable and normally don’t need much (if any), additional information to understand what went wrong.
Converting a String
into an Int
is one such example:
let value : Int? = Int("1") // 1
let value2 : Int? = Int("Hello") // nil
A String
value can either be converted to Int
or it can’t. There’s no real grey area and we don’t need any additional information to work out what went wrong.
In Swift we commonly handle simple error conditions using optional values – returning the desired value in the success case or nil
when an error condition is encountered.
Recoverable Error Conditions
Our final category of error conditions are the recoverable error conditions. Recoverable error conditions are error conditions that result from operations that fail in more complex ways and are going to be the main focus of this article.
Take the example of accessing the contents of a file on disk:
func contentsOf(file filename: String) -> String? {
// ...
}
Although here the contentsOfFile(file:)
method has been defined to return an optional value (NSString?
), in reality there are a whole range of reasons that the function may have failed.
What if the file doesn’t exist? What if we don’t have permissions to access the file? What if the file is in an incompatible format?
Although the optional return value tells us whether an error condition was encountered, there just isn’t enough information to diagnose which error condition occurred.
What we need then is a mechanism that we can use to not only indicate that an error has occurred but one that also allow us to encapsulate additional information about what went wrong so we can react to the different error conditions.
It turns out that in Swift, there are a couple of ways we can do this but before we take a look at them let’s first take a look at how errors are actually represented in Swift.
The ErrorProtocol
Protocol
In Swift, an error is a value used to report that an error condition has occurred. Error values in Swift are represented using value types that conform to the ErrorProtocol
protocol (this was renamed from the ErrorType
protocol in Swift 2.x).
The ErrorProtocol
protocol is actually an empty protocol – it doesn’t require conforming types to implement any methods nor does it require them to have any particular properties all a type has to do is declare its conformance.
Now if you’ve done any sort of Cocoa programming before, it won’t surprise you to find out that the NSError
class from the Foundation Framework already conforms to the ErrorProtocol
protocol. However, Swift also allows us to go beyond this and define error types of our own.
To define an error type of our own in Swift, all we have to do is declare a new type that conforms to the ErrorProtocol
protocol. Although we can do this with any Swift type (such as structs or classes), most commonly we use enumeration types for this task. Enumerations types are actually well suited to this for a number of reasons.
First they can be used to group together a set of related error values.
Take our file access example. We can define an new enumeration type that conforms to the ErrorProtocol
protocol and groups together the different error conditions we may encounter when accessing the contents of a file:
enum FileError : ErrorProtocol {
case notFound
case permissionDenied
case unknownFormat
}
In this example the new error type is called FileError
. As required it conforms to the ErrorProtocol
protocol and has three potential values:
.notFound
for when the file doesn’t exist, \.permissionDenied
when we don’t have permission to access the file.unknownFormat
for when the file has an unknown or incompatible format.
Secondly, we can also, should we need to, make use of enumeration cases associated values to provide additional information about the nature of any error should it occur.
Ok, so we know how to represent errors but how do we solve our earlier problem and not only indicate that an error has occurred but also communicate which error occurred? Well, one option is to encapsulate our new error type within a result type.
Using Result Types
In previous posts, We’ve talked about optional values and we’ve seen how they are, under the hood, just a generic enumeration type with two values:
enum Optional<A> {
case some(A)
case none
}
Result types are actually very similar. A result type is a generic enumeration type that also has two cases – a failure case with an associated value of a type conforming to the ErrorProtocol
protocol, and a success case with an associated value of the desired result type. In code it looks something like this:
enum ResultType {
case failure(ErrorProtocol)
case success(T)
}
Let’s apply this idea to our previous example.
Remember in our previous example we were returning an optional string (String?
) to indicate the success or failure of our file access operation:
func contentsOf(file filename: String) -> String? {
// ...
}
We can instead, re-define our function to return a ResultType
value that is parameterised over the String
type:
func contentsOf(file filename: String) -> ResultType {
//...
}
Given our definition of the ResultType
, any code now using the function can check the return value to see whether the function completed successfully or encountered an error. In addition, if an error did occur we can also determine exactly what the error was:
let filename = "source.txt"
let result = contentsOf(file: filename)
switch result {
case let .success(content):
print(content)
case let .failure(error):
switch error {
case FileError.notFound:
print("Unable to find file \(filename).")
case FileError.permissionDenied:
print("You do not have permission to access the file \(filename).")
case FileError.unknownFormat:
print("Unable to open file \(filename) - incompatible format.")
default:
print("Unknown error")
}
}
Note: Notice here that we have to include the default
case in our error switch
statement. As with all switch
statements in Swift, the switch
statement that checks the error values must be exhaustive. We must therefore ensure that we handle all values of type ErrorProtocol
.
We can however, constrain things a little further.
If we tweak the ResultType
to take two generic parameters instead of one, we can provide a type for both the associated value of the success case and for the associated value of the failure case:
enum ResultType {
case failure(E)
case success(T)
}
If we then parameterise our generic type over our more constrained FileError
type as well as the String
type:
func contentsOf(file filename: String) -> ResultType {
//…
return Result.failure(.notFound)
}
We can remove the need for our default case as the compiler knows that the errors can only be one of the three values we defined in our FileError
type:
let filename = "source.txt"
let result = contentsOf(file: filename)
switch result {
case let .success(content):
print(content)
case let .failure(error):
switch error {
case .notFound:
print("Unable to find file \(filename).")
case .permissionDenied:
print("You do not have permission to access the file \(filename).")
case .unknownFormat:
print("Unable to open file \(filename) - incompatible format.")
}
}
Now, result types are a useful tool to have in our repertoire but as of Swift 2.0 the Swift language has gained a new error handling mechanism of it’s own and one that is baked into the language.
Throwing Errors with throw
In Swift, when we want to indicate that an error condition has occurred we have two options. We can either define and return a result type as we have done above or we can throw an error using Swift’s in-built error handling mechanism.
Throwing an error is just like creating a result type, it allows us to create a value that represents the encountered error condition and potentially attach additional information to that error. It won’t surprise you to find out that we do this using a throw
statement.
The throw
statement consists of the throw
keyword followed by an expression whose value conforms to the ErrorProtocol
protocol. For example:
throw FileError.notFound
In Swift, throw
statements can occur in the body of any function or method or within the body of any closure expression. (To make things easier I’m just going to refer to these all as functions for now). When executed, throw
statements cause execution of the current program scope to immediately end and the thrown error value to be propagated to the enclosing scope.
Error Handling the 10,000ft View
Ok. So far so good. We know how to throw an error but what do we do when these error values are actually thrown?
Generally speaking errors are not what we want to encounter in our code and when errors do occur (and trust me, they will) it is up to us to write code to detect and handle those errors.
The act of handling an error in Swift is known as catching it. So we’ve got throwing to create and propagate an error and catching to detect and handle the error. Throw. Catch. Get it?
Anyway, exactly where and when we handle errors in our code is up to us but there is one golden rule – not handling errors – ignoring them and hoping they go away – is not an option. The Swift compiler is actually pretty strict on this topic and requires that we handle all errors at some point in our chain of function calls – so much so that it won’t compile our code if we don’t!
Ok, so we have to handle and catch errors when they’re thrown but how exactly do we do that?
In Swift, we have two main courses of action when it comes to catching and handling errors:
- Propagate the error to another function further up the call chain (a.k.a. make some other piece of code handle it).
- Handle the error within the scope of the current function.
Which approach we take is dependent on what makes most sense for the particular situation but in the next few sections, we’ll take a closer look at each so we can make a more informed decision about which to choose. Let’s start by looking at how to propagate errors.
Propagating Errors
Error Propagation with throws
In Swift, whether or not a function can propagate an error is baked into the functions signature.
By default, a function in Swift cannot propagate errors. This applies to all types of function whether they be global functions, methods or closures. It also means that we must, by default, write code to catch any errors that may occur and handle them within the body of a function in which they occur.
Note: In the following sections I again refer to functions but this equally applies to methods and closures as well.
Despite these rules, sometimes it’s just not possible to handle errors within the scope of the current function. Maybe we don’t have enough information to handle it at that level or maybe it just doesn’t makes sense to do so. Whatever the reason, there are occasions when we want to propagate errors that have occurred within a function back to the code that called that function.
To indicate that a function may propagate an error back to it’s calling scope, Swift requires that we annotate such functions with the throws
(or as we’ll see shortly – rethrows
) keyword. Functions that are annotated in this manner are unsurprisingly known as throwing functions.
To annotate a function with the throws
(or rethrows
) keyword we write the keyword after the parameters in the functions declaration and, if the function returns a value, before the return arrow (->
). For example, we could re-write our fileContents(filename:)
example as follows:
func contentsOf(file filename: String) throws -> String {
//...
}
Calling Throwing Functions
In the previous section we looked at annotating a function with the throws
(or as we’ll see later the rethrows
) keyword in order to propagate errors from the function. The thing is, annotating the function in this manner also has a second purpose – it makes it crystal clear for anybody reading the signature of the function that that function may propagate an error back to it’s calling code.
However there is something missing. If we look at it from a different perspective, the perspective of the code that called the function, things are a little less clear:
let content = contentsOf(file: "test.txt")
Here we not really sure whether the function will propagate an error or not. This lack of clarity was recognised as an issue within the language and to make things clear from the perspective of the calling code, the Swift language also mandates that when we call a function that can propagate an error, we have to prefix that function call with the try
keyword (or as we’ll see the try?
or try!
keywords). Here’s an example:
let content = try contentsOfFile(file: "test.txt")
The inclusion of the try
keyword does two things. First it signals to the compiler that we know we are calling a function that may propagate an error and secondly (and probably more importantly), it makes it clear to anyone reading our code that the function being called may throw an error.
Ok, let’s look at a more advanced example.
Throwing Functions and Inheritance
Note: This section is a little more advanced and may be challenging if you haven’t got any previous experience of object-oriented design principles. Don’t feel disheartened by this though. Every great developers was at that point at one point or other. Just skip this section for now and come back to it later – I won’t be offended 😉
Still here? Good. Ok super-quick object-oriented design re-cap.
As you probably know, inheritance is an object-oriented concept where functionality in one class can inherit the behaviour and properties of a parent or super class. You’ll also probably know that as part of that inheritance mechanism, it is also possible for child classes to override the behaviour of the methods in the parent class.
Now, you might be wondering why I mention this, but it’s an important consideration when it comes to error handling in Swift.
Imagine the scenario. We have a parent class (Foo
) that has some method doSomething()
:
class Foo {
func doSomething() -> String {
return "Foo"
}
}
Notice that doSomething()
method is not annotated with the throws
keyword. As we’ve learnt this means that any errors that occur within the body of the doSomething()
method, must be handled within that method and any code calling the function is safe in the knowledge that no errors will be propagated.
Now imagine we are overriding that method in the child class (Bar
).
As we know, to override a method in the child class, the method in the child class must have the same signature as that of the parent class in order to override it. However, suppose we tried to annotate the method with the throws
keyword:
class Bar : Foo {
override func doSomething() throws -> String {
return "Bar"
}
}
The thing is, this code doesn’t compile.
In the context of error handling, Swift doesn’t allow us to override a non-throwing method in a parent class with a throwing method in a child class. The same rule applies when attempting to satisfy a protocol requirement for a non-throwing method with a method that actually throws.
If you think about it, in both cases it’s not that surprising.
Think about the code that might be calling the doSomething()
method on the parent (Foo
) class. None of the code is expecting the call to the method to result in an error being propagated, after all it’s not annotated with the throws
keyword and we’ve already talked about how only methods annotated with throws
can propagate errors. As we know though, due to the wonders of object-oriented polymorphism, an instance of a child class can be cast to an instance of a parent class. This makes things a little complicated.
Imagine if the child class Bar
was cast to an instance of the parent class Foo
and the doSomething()
method was called. Code calling that function would not expect the code to throw an error but the method of the underlying class (the child class Bar
) could actually throw – a real surprise to any of the calling code. The Swift language doesn’t like nasty surprises and to avoid this whole problem, overriding in this way is not allowed in Swift.
However, consider this scenario.
Imagine we had another class (Baz
) with a method that was already marked as throwing:
class Baz {
func doSomethingElse() throws -> String {
return "Foo"
}
}
Now imagine we had a child class (Qux
) that inherited from Baz
and overrode the doSomethingElse()
method:
class Qux : Baz {
override func doSomethingElse() -> String {
return "Bar"
}
}
Notice here that the doSomethingElse()
method is NOT marked as throwing in the child class. It might surprise you but this code does compile and is in fact perfectly legal.
If you think about it this again pretty logical. Consider the calling code again.
Any code that was calling the doSomethingElse()
method in the parent (Baz
) class already knows that there is a potential for the doSomethingElse()
method to propagate an error. That means that that code will have been written to the cases where either an error is propagated from the method call or no error occurs.
If, as in our previous scenario, the child class (Qux
) is then cast to an instance of the parent class (Baz
) and the doSomethingElse()
method is called, the fact that the doSomethingElse()
method doesn’t raise an error is already handled by within the calling code. No surprises and no problems.
If we abstract this away a little, the point is that in Swift, functions that cannot throw, are actually subsets of functions that can throw. This means that we can use a non-throwing function, anywhere we can use a throwing function but not the reverse.
Propagating Errors with rethrows
Now that we’re clear about the relationship between throwing functions and inheritance, let’s look at a slightly different scenario.
Imagine we had a simple logging function log
that takes a single parameter – a closure that returns the string to be logged:
func log(description:() -> String) -> Void {
print(description())
}
In this form, everything works fine. However, now imagine that the closure we supplied to the log
function could itself throw an error. What are the implications?
Well, if we called the closure within the body of the log
function it obviously might result in an error being thrown. We could modify the log
function to handle the error but equally we might want to propagate that error back to the calling function. This is where rethrows
comes in.
In Swift the rethrows
keyword is used to indicate that a function throws an error if and only if, one of its parameters throws an error. Functions or methods annotated in this manner are known as rethrowing functions or methods. Here’s an example:
func log(description: () throws -> String) rethrows -> () {
print(try description())
}
func nonThrowing() -> String {
return "Hello"
}
enum LogError : ErrorType {
case someError
}
func throwing() throws -> String {
// Do stuff....
throw LogError.someError
}
log(nonThrowing)
try log(throwing) // Has to be called with 'try' because it might throw
In the first call to the log()
function, the closure we supply as an argument (in this case the function nonThrowing()
) doesn’t throw an error and therefore, due to the rethrows
annotation, the log()
function also doesn’t throw an error. This means we don’t need to prefix the call with the try
keyword and are guaranteed that no error will be propagated.
Notice though that in the second call to the log()
function, we do have to include the try
keyword. In this case, because the closure we pass in as an argument throws an error, the log()
function in turn throws an error. As a result of this, we therefore have to include the try
keyword before the call to acknowledge that an error may be thrown.
Now, there are a few things to note with the rethrows
keyword.
Firstly, rethrowing functions MUST take at least one throwing function as an argument. The compiler will complain if the don’t:
func baz(completion: () -> String) rethrows -> String {
return completion()
}
// ERROR: 'rethrows' function must take a throwing function argument
Secondly, throwing functions can only propagate errors that are thrown by the functions they take as arguments or errors that propagate from enclosed scopes. This means that rethrowing functions can’t contain any throw
statements directly:
enum ExampleError : ErrorProtocol {
case someError
case someOtherError
}
func abc(completion: () -> String) rethrows -> String {
throw ExampleError.someError
}
// ERROR: a function declared 'rethrows' may only throw if its parameter does
They can however catch errors using a do-catch
statement (we’ll cover these shortly) and re-throw either the same or a different error which is then propagated to the enclosing scope:
func qux(completion: () throws -> String) rethrows -> String {
do {
return try completion()
} catch {
print("Inside Qux: \(error)")
throw ExampleError.someOtherError
}
}
do {
print(try qux { () throws -> String in
throw ExampleError.someError
})
} catch {
print("Outside Qux: \(error)")
}
Note: If you’re a bit confused about the do-catch
statement don’t worry, we’ll be looking at them in more detail shortly.
When it comes to rethrowing functions and inheritance, similar rules apply to those that we looked at earlier with throwing vs non-throwing functions.
Firstly, throwing methods can’t override rethrowing methods and throwing methods can’t satisfy protocol requirements for a rethrowing methods. This is for the same reasons as we looked at earlier. Code calling the throwing method knows whether the function will throw based on whether the functions supplied as an argument throw. If we overrode the function to always throw, the calling code might get some nasty surprises.
Conversely, rethrowing methods can override a throwing method and a rethrowing method can also satisfy a protocol requirement for a throwing method though. As we saw earlier, non-throwing methods are simply a subset of all throwing methods. This means that the calling code is already setup to handle any errors that may be thrown. If the method overridden method doesn’t throw a method then great, it doesn’t matter from the perspective of the calling code.
Handling Errors
Ok, let’s park error propagation for now and look at how we can actually handle errors rather than simply propagating the errors to an enclosing scope.
When it comes to handling errors we have three main options:
- Handle the error in a
do-catch
statement. - Handle the error as an optional value.
- Assert that the error will never occur.
As we’ve already touched on the do-catch
statement in the previous section let’s start with that.
Handling Errors with do-catch
Statement
In Swift, the do
statement is used to introduce a new scope. It is similar to the curly braces ({}
) in C and is used to delimit a new code block. The scope that the do
statement introduces is just like any other scope. This means that variables and constants declared within that scope can only be accessed within that scope and go out of scope when the do
statement ends. Now, you may be wondering why I mention the do
statement, after all this is an article on error handling. The things is, the do
statement has a trick up it’s sleeve. In addition to introducing a new scope, the do
statement can optionally contain one or more catch
clauses.
Each catch
clause contains a pattern that can be used to match against defined error conditions. The catch
clauses allows you to catch a thrown error value and handle that error.
From a high-level then, the do-catch
statement has the following general form:
do {
try expression
statements
}
}
catch pattern 1 {
statements
}
catch pattern 2 where condition {
statements
}
catch {
statements
}
We can use this in various forms to to catch different types of error should they occur:
enum InputError : ErrorProtocol {
case makeMissing
case mileageTooLow(Int)
case mileageTooHigh(Int)
}
func shouldBuyCar(make: String, mileage: Int) throws {
guard make.characters.count > 0 else {
throw InputError.makeMissing
}
switch mileage {
case mileage where mileage < 10:
throw InputError.mileageTooLow(mileage)
case mileage where mileage > 100:
throw InputError.mileageTooHigh(mileage)
default:
print("Buy it!")
}
}
do {
try shouldBuyCar(make: "Honda", mileage:120)
}
catch InputError.makeMissing {
print("Missing make")
}
catch let InputError.mileageTooHigh(x) where x > 150 {
print("Mileage way way too high...")
}
catch let InputError.mileageTooHigh(x) {
print("Mileage too high")
}
catch {
print("\(error)")
}
// Mileage too high
It’s a bit of a long example so let’s walk through it.
So, the scenario is that we’re buying a car and we have a little function that takes in some details and decides whether to buy the car or not. (Not the best way of doing it I’m sure but it’ll do for our example).
At the top, we then have the shouldBuyCar(make:mileage:)
function. This function takes in the different parameters (the make and mileage) and decides whether we should buy the car. In this case the shouldBuyCar(make:mileage:)
function is marked as throwing as it can potentially throw one or three errors.
- If we supply an empty string for the cars make it will throw a
.makeMissing
error. - If the mileage is too low it will throw a
.mileageToLow
error and will attach the offending mileage as an associated value. - Finally if the mileage is too high it’ll throw a
.mileageToHigh
error again attaching the mileage that caused the issue.
In all other cases, it simply prints out the Buy it!
statement to the console.
Now, the meat of what we want to look at is actually at the bottom of the example.
First, we have our do
statement and within the body of the statement we make our call to the shouldBuyCar(make:mileage:)
function prefixing it with the try
keyword due to the fact that the function call may result in an error being thrown.
After that, we have a number of catch
clauses. Each of these catch
clauses contains a different pattern that will match against any error that is propagated from the call to the shouldBuyCar(make:mileage:)
function.
First we use the identifier pattern to check for an explicit error value. This will match any and all makeMissing
errors e.g.:
catch Car.InputError.makeMissing {
// Handle the error.
print("Missing make")
}
We also use an enumeration case pattern to match the .mileageTooHigh
error value and extract any associated mileage:
catch InputError.mileageTooHigh(x) {
print("Mileage too high")
}
We can also combine that with a where
clause to further constrain things as we’ve done in the second catch clause:
catch let InputError.mileageTooHigh(x) where x > 150 {
print("Mileage way way too high...")
}
Note: The catch
clauses are evaluated sequentially, much like in a switch
statement and will execute the first catch
clause that matches. In this case we have to include our more contained catch
clause containing the where
clause before it’s more generic sibling if it is to ever match.
One other more subtle point to be aware of is that as with switch
statements, the Swift compiler also attempts to infer whether the catch
clauses in a do-catch
statement are exhaustive. If a determination can be made, the compiler views the error has being handled. If however the catch
clauses are not exhaustive, the error automatically propagates out of the containing scope, either to an enclosing catch clause or out of the function. As you might expect, if the error is propagated out of the function, the function must then be marked with the throws
keyword to alert calling code of that possibility.
So that leaves the question of how to ensure that we have handled all potential errors.
We actually have a number of options for achieving this. First we can use a catch clause with a pattern that matches all errors, such as a wildcard pattern (_
). The issue with this approach though is that we don’t get access the error value itself it is simply swallowed.
As an alternative, we can however use the catch
clause on it’s own as I’ve done in the last catch
clause in this example. When used in this manner the catch
clause automatically matches and binds any error that it encounters to a local constant named error
. This error
value can then be accessed within the body of that catch
clause.
Converting Errors Into Optional Values with try?
In addition to the do-catch
statement, Swift also provides us with a couple of other options when it comes to handling errors. The first of these is the try?
statement. We’ve already seen it’s sibling the try
statement earlier in this article.
As you know, the try
statement is used in front of calls to functions, methods or closures that may throw an error. try?
is used in the same place however it’s behaviour is a little different. With the try?
statement, if an error is thrown whilst evaluating the associated expression (such in a call to a throwing function) the result of the expression is set to nil
rather than propagating the error back to the enclosing scope. Essentially this allows us to handle the error and convert it into an optional value in one move:
func throwingFunction() throws -> String {
// ...
}
let result = try? throwingFunction()
// Result is an String?
As with other techniques we’ve seen though, this approach does have it’s shortcomings the main one being that you don’t get any information about the exact error that occurred. However, in certain situations, this may be just what you want so it’s worth knowing about.
Preventing Error Propagation with try!
Our final option when it comes to error handling is the try!
statement. This is the more assertive version of it’s siblings.
Sometimes we are writing our code we know that a function won’t in fact throw an error at runtime despite the function being marked as such.
Maybe we have pre-validated the input data, maybe the circumstances are such that the error simply won’t be thrown. Whatever the reason, in these occasions we can use the try!
keyword before a potentially throwing expression to disable the propagation of errors and wrap the call in a runtime assertion that no error will be thrown.
For example, say we had a function that threw an error if it was passed a negative value (and yes, I know we could define the parameter as UInt
but go with it). We might choose to use the try!
statement if we had already pre-validated that the argument we were passing to the function was definitely positive:
enum ValueError : ErrorProtocol {
case negativeValue
}
func squarePositive(value: Int) throws -> Int {
guard value >= 0 else {
throw ValueError.negativeValue
}
return value * value
}
let output : Int
let input = 10
if input >= 0 {
output = try! squarePositive(value: input)
print(output)
}
A contrived example I know, but you get the idea. The only thing to mention is that you need to be a little careful with the try!
statement. As I mentioned, the try!
statement wraps the call to the throwing function in a run-time assertion. This means you need to be absolutely sure that no error will be thrown otherwise you risk your code crashing at runtime.
Error Propagation With Asynchronous Closures
The final thing I want to look at today is error handling and asynchronous operations. It’s another more advanced topic so skip this if you’re just getting started. Also, I’m not going to go into this in huge depth. This article is already pretty long but I wanted to mention it so that you had some idea about the possibilities. Anyway, let’s dip our toe in.
Asynchronous operations, as the name suggests, are where there is a delay between the initial call to a function and the response coming back. In Swift, this is commonly implemented through the use of completion handlers:
func asyncOperation(completion: String -> ()) -> Void {
DispatchQueue.global(attributes: .qosDefault).async {
// 1. Do stuff...
let result = "Hello world"
DispatchQueue.main.async {
completion(result)
}
}
}
func completionHandler(result: String) {
print(result)
}
asyncOperation(completion: completionHandler)
// Hello world
Note: If you’re trying the above example in a Playground you’ll also have to add the following at the top of your playground page to ensure the playground provides enough time for the asyncOperation(completion:)
function to complete:
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true
Now, think about error handling and this example above. What if an error occurred within the body of the asynchronous operation? How could we handle it?
Well, with everything we’ve covered up until now, you should be armed with most of the tools you’d need. One option is to simply handle the error within the body of the asyncOperation(completion:)
function by using a do-catch
statement. Notice here that I also modify the completion handler to take an optional value which will be set to nil
in the case of an error or the result otherwise:
enum ExampleError: ErrorProtocol {
case error
}
func throwingOperation() throws -> String {
throw ExampleError.error
}
func asyncOperation(operation:() throws -> String,
completion:String? -> ()) -> Void {
DispatchQueue.global(attributes: .qosDefault).async {
// Do stuff..
var result: String? = nil
do {
result = try operation()
} catch {
print(error)
}
DispatchQueue.main.async {
completion(result)
}
}
}
func completionHandler(result: String?) {
print(result)
}
asyncOperation(operation: throwingOperation, completion: completionHandler)
Now this is fine, but what if we wanted to return any error that occurred back to the calling scope?
Well another option is to go back to the start of this article, and make use of the ResultType
we talked about:
enum ExampleError: ErrorProtocol {
case someError
}
enum ResultType<T> {
case err(ErrorProtocol)
case success(T)
}
func throwingOperation() throws -> String {
throw ExampleError.someError
}
func asyncOperation(operation:() throws -> String,
completion:(ResultType<String>) -> ()) -> Void {
DispatchQueue.global(attributes: .qosDefault).async {
// Do stuff..
var result: ResultType<String>
do {
let output = try operation()
result = .success(output)
} catch {
result = .err(error)
}
DispatchQueue.main.async {
completion(result)
}
}
}
func completionHandler(result: ResultType<String>) {
switch result {
case let .success(value):
print(value)
case let .err(error):
print(error)
}
}
asyncOperation(operation: throwingOperation, completion: completionHandler)
Another option, and one that is possible but feels like a bit of a hack is to us an inner closure:
enum ExampleError: ErrorProtocol {
case someError
}
enum ResultType<T> {
case err(ErrorProtocol)
case success(T)
}
func throwingOperation() throws -> String {
throw ExampleError.someError
}
func asyncOperation(operation:() throws -> String,
completion: (innerclosure: () throws -> String) -> Void) -> Void {
DispatchQueue.global(attributes: .qosDefault).async {
// Do stuff..
var result: () throws -> String
do {
let output = try operation()
result = { output }
} catch {
result = { throw error }
}
DispatchQueue.main.async {
completion(innerclosure: result)
}
}
}
func completionHandler(result: () throws -> String) {
do {
let value = try result()
print(value)
} catch {
print(error)
}
}
asyncOperation(operation: throwingOperation, completion: completionHandler)
Here, instead of returning a ResultType
we return a closure that encapsulates the result of the async operation. In the case of success, the returned closure will return the result. In case of error, the returned closure will (re?)throw the original error value. The calling code can then extract this result using Swift’s normal do-catch
handling. The main downside with this inner-closure approach is that it doesn’t work with methods you don’t own. You’ll notice that in order to get this to work, the signature of the completion handler needs to be modified to accept a closure rather than a normal return value. This is works well if you own that code, but if you’re using one of the standard library functions then not so much. Despite this I thought it was worth mentioning as it’s good to have this encapsulated closure approach as an option.
Wrapping Up
Ok, that pretty much wraps it up for error handling in Swift.
We’ve looked at the different types of error that can occur, seen how to throw errors of our own and then spent the latter part of this article looking at how we can handle those errors when they do occur.
As we’ve seen, error handling took two major forms, either propagating errors or handling them using either the do-catch
, try?
or try!
statements. Whatever your approach, hopefully this article will have given you a much better understanding of error handling in Swift and will allow you to start leveraging it’s power within your own code.