Handling errors

If we try to call a function, such as normal, Swift is going to give us an error, as shown in the following example:

let repeated1 = repeatString("Hello", untilLongerThan: 20)
// Error: Call can throw but is not market with 'try'

To eliminate this error, we must add the try keyword before the call. However, before we move forward, I would recommend that you wrap all of your code inside a function, if you are following along in a playground. This is because throwing errors at the root level of a playground will not be handled properly and may even cause the playground to stop working. To wrap your code in a function, you can simply add the following code:

func main() {
// The rest of your playground code
}
main()

This defines a function called main that contains all the normal playground code that is called once, at the end of the playground.

Now, let's get back to using the try keyword. There are actually three forms of it: try, try?, and try!. Let's start by discussing the exclamation point form, as it is the simplest form.

Forceful try

The try! keyword is called the forceful try. The error will completely go away if you use it, by using the following code:

let repeated2 = try! repeatString("Hello", untilLongerThan: 20)
print(repeated2) // "HelloHelloHelloHello"

The drawback of this approach might be intuitive, based on the exclamation point and what it has meant in the past. Just like with forced unwrapping and forced casting, an exclamation point is a sign that there will be a scenario which will crash the entire program. In this case, the crash will be caused if an error is thrown from the function. There may be times when you can really assert that an error will never be thrown from a call to a throwing function or method, but in general this isn't an advisable solution, considering the fact that we are trying to gracefully handle our error situations.

Optional try

We can also use the try? keyword, which is referred to as an optional try. Instead of allowing for the possibility of a crash, this will turn the result of the function into an optional:

let repeated3 = try? repeatString("Hello", untilLongerThan: 20)
print(repeated3) // Optional("HelloHelloHelloHello")

The advantage here is that if the function throws an error, repeated3 will simply be set to nil. However, there are a couple strange scenarios with this. First, if the function already returns an optional, the result will be converted to an optional of an optional:

func aFailableOptional() throws -> String? {
    return "Hello"
}
print(try? aFailableOptional()) // Optional(Optional("Hello"))

This means that you will have to unwrap the optional twice in order to get to the real value. The outer optional will be nil if an error is thrown and the inner optional will be nil if the method returned nil.

The other strange scenario is if the function doesn't return anything at all. In this case, using an optional try will create an optional void, as shown:

func aFailableVoid() throws {
    print("Hello")
}
print(try? aFailableVoid()) // Optional(())

You can check the result for nil to determine if an error was thrown.

The biggest drawback to this technique is that there is no way to determine the reason an error was thrown. This isn't a problem for our repeatString:untilLongerThan: function because there is only one error scenario, but we will often have functions or methods that can fail in multiple ways. Especially, if these are called based on user input, we will want to be able to report to the user exactly why an error occurred.

To allow us to get more precise information on the reason for an error, we can use the final keyword, which is simply try.

Catching an error

To get an idea of the usefulness of catching an error, let's look at writing a new function that will create a list of random numbers. Our function will allow the user to configure how long the list should be and also what the range of possible random numbers should be.

The idea behind catching an error is that you get a chance to look at the error that was thrown. With our current error type, this wouldn't be terribly useful because there is no way to create different types of errors. A great option to fix this is to use an enumeration that implements the ErrorType protocol:

enum RandomListError: ErrorType {
    case NegativeListLength
    case FirstNumberMustBeLower
}

This enumeration has a case for both the errors which we will want to throw, so now we are ready to implement our function:

func createRandomListContaininingXNumbers(
    xNumbers: Int,
    between low: Int,
    and high: Int
    ) throws -> [Int]
{
    guard xNumbers >= 0 else {
        throw RandomListError.NegativeListLength
    }
    guard low < high else {
        throw RandomListError.FirstNumberMustBeLower
    }

    var output = [Int]()
    for _ in 0 ..< xNumbers {
        let rangeSize = high - low + 1
        let betweenZero = Int(rand()) % rangeSize
        let number = betweenZero + low
        output.append(number)
    }
    return output
}

This function begins by checking the error scenarios. It first checks to make sure that we are not trying to create a list of negative length. It then checks to make sure that the high value of the range is in fact greater than the low one. After that, we repeatedly add a random number to the output array for the requested number of times.

Note that this implementation uses the rand function, which we used in Chapter 2, Building Blocks – Variables, Collections, and Flow Control. To use it, you will need to import Foundation and also seed the random number with srand again.

Also, this use of random is a bit more complicated. Previously, we only needed to make sure that the random number was between zero and the length of our array; now, we need it to be between two arbitrary numbers. First, we determine the amount of different numbers we can generate, which is the difference between the high and low number plus one, because we want to include the high number. Then, we generate the random number within that range and finally, shift it to the actual range we want by adding the low number to the result. To make sure this works, let's think through a simple scenario. Lets say we want to generate a number between 4 and 10. The range size here will be 10 - 4 + 1 = 7, so we will be generating random numbers between 0 and 6. Then, when we add 4 to it, it will move that range to be between 4 and 10.

So, we now have a function that throws a couple of types of errors. If we want to catch the errors, we have to embed the call inside a do block and also add the try keyword:

do {
    try createRandomListContaininingXNumbers(
        5,
        between: 5,
        and: 10
    )
}

However, if we put this into a playground, within the main function, we will still get an error that the errors thrown from here are not handled. This will not produce an error if you put it at the root level of the playground because the playground will handle any error thrown by default. To handle them within a function, we need to add catch blocks. A catch block works the same as a switch case, just as if the switch were being performed on the error:

do {
    try createRandomListContaininingXNumbers(
        5,
        between: 5,
        and: 10
    )
}
catch RandomListError.NegativeListLength {
    print("Cannot create with a negative number of elements")
}
catch RandomListError.FirstNumberMustBeLower {
    print("First number must be lower than second number")
}

A catch block is defined with the keyword catch followed by the case description and then curly brackets that contain the code to be run for that case. Each catch block acts as a separate switch case. In our preceding example, we have defined two different catch blocks: one for each of the errors where we print out a user-understandable message.

However, if we add this to our playground, we still get an error that all errors are not handled because the enclosing catch is not exhaustive. That is because catch blocks are just like switches in that they have to cover every possible case. There is no way to say if our function can only throw random list errors, so we need to add a final catch block that handles any other errors:

do {
    try createRandomListContaininingXNumbers(
        5,
        between: 5,
        and: 10
    )
}
catch RandomListError.NegativeListLength {
    print("Cannot create with a negative number of elements")
}
catch RandomListError.FirstNumberMustBeLower {
    print("First number must be lower than second number")
}
catch let error {
    print("Unknown error: (error)")
}

The last catch block stores the error into a variable that is just of type ErrorType. All we can really do with that type is print it out. With our current implementation this will never be called, but it is possible that it will be called if we add a different error to our function later and forget to add a new catch block.

Note that currently there is no way to specify what type of error can be thrown from a specific function; so with this implementation there is no way for the compiler to ensure that we are covering every case of our error enumeration. We could instead perform a switch within a catch block, so that the compiler will at least force us to handle every case:

do {
    try createRandomListContaininingXNumbers(
        5,
        between: 5,
        and: 10
    )
}
catch let error as RandomListError {
    switch error {
    case .NegativeListLength:
        print("Cannot create with a negative number of elements")
    case .FirstNumberMustBeLower:
        print("First number must be lower than second number")
    }
}
catch let error {
    print("Unknown error: (error)")
}

This technique will not cause the compiler to give us an error if we throw a completely different type of error from our function, but it will at least give us an error if we add a new case to our enumeration.

Another technique that we can use would be to define an error type that includes a description that should be displayed to a user:

struct UserError: ErrorType {
    let userReadableDescription: String
    init(_ description: String) {
        self.userReadableDescription = description
    }
}

func createRandomListContaininingXNumbers2(
    xNumbers: Int,
    between low: Int,
    and high: Int
    ) throws -> [Int]
{
    guard xNumbers >= 0 else {
        throw UserError(
            "Cannot create with a negative number of elements"
        )
    }
    
    guard low < high else {
        throw UserError(
            "First number must be lower than second number"
        )
    }

    // ...
}

Instead of throwing enumeration cases, we are creating instances of the UserError type with a text description of the problem. Now, when we call the function, we can just catch the error as a UserError type and print out the value of its userReadableDescription property:

do {
    try createRandomListContaininingXNumbers2(
        5,
        between: 5,
        and: 10
    )
}
catch let error as UserError {
    print(error.userReadableDescription)
}
catch let error {
    print("Unknown error: (error)")
}

This is a pretty attractive technique but it has its own drawback. This doesn't allow us to easily run certain code if a certain error occurs. This isn't important in a scenario where we are just reporting the error to the user, but it is very important for scenarios where we might more intelligently handle errors. For example, if we have an app that uploads information to the Internet, we will often run into Internet connection problems. Instead of just telling the user to try again later, we can save the information locally and automatically try to upload it again later without having to bother the user. However, Internet connectivity won't be the only reason an upload might fail. In other error circumstances, we will probably want to do something else.

A more robust solution might be to create a combination of both of these techniques. We can start by defining a protocol for errors that can be reported directly to the user:

protocol UserErrorType: ErrorType {
    var userReadableDescription: String {get}
}

Now we can create an enumeration for our specific errors that implements that protocol:

enum RandomListError: String, UserErrorType {
    case NegativeListLength =
        "Cannot create with a negative number of elements"
    case FirstNumberMustBeLower =
        "First number must be lower than second number"

    var userReadableDescription: String {
        return self.rawValue
    }
}

This enumeration is set up to have a raw type that is a string. This allows us to write a simpler implementation of the userReadableDescription property that just returns the raw value.

With this, our implementation of the function looks the same as earlier:

func createRandomListContaininingXNumbers3(
    xNumbers: Int,
    between low: Int,
    and high: Int
    ) throws -> [Int]
{
    guard xNumbers >= 0 else {
        throw RandomListError.NegativeListLength
    }
    guard low < high else {
        throw RandomListError.FirstNumberMustBeLower
    }

    // ...
}

However, our error handling can now be more advanced. We can always just catch any UserErrorType and display it to the user, but we can also catch a specific enumeration case if we want to do something special in this scenario:

do {
    try createRandomListContaininingXNumbers3(
        5,
        between: 5,
        and: 10
    )
}
catch RandomListError.NegativeListLength {
    // Do something else
}
catch let error as UserErrorType {
    print(error.userReadableDescription)
}
catch let error {
    print("Unknown error: (error)")
}

Keep in mind that the order of our catch blocks is very important, just like the order of switch cases is important. If we put our UserErrorType block before the NegativeListLength block, we would always just report it to the user, because once a catch block is satisfied, the program will skip every remaining block.

This is a pretty heavy handed solution; so, you may want to use a simpler solution at times. You may even come up with your own solutions in the future, but this gives you some options to play around with.

Propagating errors

The last option for handling an error is to allow it to propagate. This is only possible when the containing function or method is also marked as throwing errors, but it is simple to implement if that is true:

func parentFunction() throws {
    try createRandomListContaininingXNumbers3(
        5,
        between: 5,
        and: 10
    )
}

In this case, the try call does not have to be wrapped in a do-catch, because all errors thrown by createRandomListContainingXNumbers:between:and: will be rethrown by parentFunction. In fact, you can still use a do-catch block, but the catch cases no longer need to be exhaustive, because any errors not caught will simply be rethrown. This allows you to only catch the errors relevant to you.

However, while this can be a useful technique, I would be careful not to do it too much. The earlier you handle the error situations, the simpler your code can be. Every possible error thrown is like adding a new road to a highway system; it becomes harder to determine where someone took a wrong turn if they are going the wrong way. The earlier we handle errors, the fewer chances we have to create additional code paths in the parent functions.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset